Wednesday, September 22, 2021

Haku: A toy functional programming language based on literary Japanese

Haku

A toy functional programming language based on literary Japanese.

Is Haku for you?

  • Haku lets you write programs that look very much like written Japanese. So you need to be familiar with written Japanese to program in Haku.
  • Haku is an experiment, not a practical programming language. Several of its features are rather contrary.
  • The implementation is incomplete and buggy, and error messages are poor.

Requirements

To run Haku you'll need to install the Raku programming language. If you plan to use Raku (it is a wonderful language), I recommend you use the Rakubrew installation tool.

Running Haku

I am assuming you'll run Haku on command line, in the directory cloned from Git or where you unzipped the downloaded archive.

In that directory is a script haku. Example programs are in the subdirectory examples (horizontal writing) and examples/tategaki (vertical writing).

Usage: haku <Haku program, written horizontally or vertically, utf-8 text file>
    [--tategaki, -t] : do not run the program but print it vertically.
    [--miseru, -m] : just print the Raku source code, don't execute.
    [--yomudake, -y] : just print the Haku source after reading, as a single line. Don't execute.
  • The tategaki options is a pretty-printer but the generated code is valid Haku: if you save the result in a text file, haku can read it in and run it.
  • The miseru option shows the Raku code generated by the compiler.
  • The yomudake option prints the string that the compiler uses as input, i.e. the program source converted to a single line.

Example:

$ ./haku examples/tategaki/iroha_tategaki.haku

This should print

7188
557188

The first time you run haku will take quite a long time (several minutes) because Raku needs to compile the modules to bytecode. After that, it should only take a few seconds.

Haku by example

Example 1: Iroha

Consider the following Haku program:

註 例のはくのプログラム。

本とは
ラムダは或エクスでエクス掛けるエクスです、
カズ達は八十八と七千百と五十五で、
イ・ロ・ハ・空はカズ達で、
シンカズはイとロの和で、
シンカズを見せる、
ケッカは〈七百四十壱をラムダする〉足す九百十九、
【ケッカとシンカズの和】を見せる
のことです。

which compiles to the following Scheme code:

(define (displayln str) (display str) (newline))(define (hon)

; 例のはくのプログラム    
(define (hon)
    (let* (
            (RAMUDA (lambda (EKUSU) (* EKUSU EKUSU )))
            (KAZUTACHI (list 88 7100 55))
            (I (car KAZUTACHI))
            (RO (cadr KAZUTACHI))
            (HA (caddr KAZUTACHI))
            (SHINKAZU (+ I RO ))
            (KEKKA (+ (RAMUDA 741) 919 ))
        )
        (displayln SHINKAZU)
        (displayln (+ KEKKA SHINKAZU ))
    )
)

(hon)

Let's take it apart:

註 ... 。

is a comment.

The main program (called 本, hon) has a fixed begin and end string:

本とは
...
のことです。

In Romaji this reads "Hon to wa ... no koto desu.", roughly "Main is the following thing(s): ...".

In Scheme I emit a function as body of Hon a let*-binding (i.e. binding is sequential):

(define (hon)
    (let* (
        ...
        )
        ...
    )
)

In the example we have an number of different types of assignments:

ラムダは或エクスでエクス掛けるエクスです、

"RAMUDA wa aru EKSU de EKSU kakeru EKSU desu"

Katakana is for variables, kanji for functions and keywords, hiragana for keywords and verb endings (e.g. in 掛ける and 見せる).

This roughly reads as "as for RAMUDA, with a given X it is X times X", so RAMUDA binds to a lambda function. In Scheme this becomes:

(RAMUDA (lambda (EKUSU) (* EKUSU EKUSU )))

Next we have an assignment to a list of number constants:

カズ達は八十八と七千百と五十五で、

"KAZUTachi wa 88 to 7100 to 55 de,"

Numbers are written in kanji. The particle to is the list separator. You can use 達 tachi to show that a variable name is plural. In Scheme this becomes:

(KAZUTACHI (list 88 7100 55))

Next we have a bit of syntactic sugar borrowed from Haskell (cons):

イ・ロ・ハ・空はカズ達で、

"I:RO:HA:Kuu wa KAZUTachi de,"

kuu means "empty". This means that the list is deconstructed into elements I, RO, HA and and empty list. Scheme does not have this kind of pattern matching so each assignment is generated separately.

The next assignment,

シンカズはイとロの和で、

"SHINKAZU wa I to RO no Wa de"

is simply

"SHINKAZU is the sum of I and RO"

(SHINKAZU (+ I RO ))

Then we have a print statement:

シンカズを見せる、

"SHINKAZU wo Miseru"

"To show SHINKAZU"

In Scheme:

(displayln SHINKAZU)

Then follows another assignment:

ケッカは〈七百四十壱をラムダする〉足す九百十九、

"KEKKA wa (741 wo RAMUDA suru) Tasu 919"

"KEKKA is (RAMUDA of 741) plus 919

(KEKKA (+ (RAMUDA 741) 919 ))

And finally we show the result of an expression:

【ケッカとシンカズの和】を見せる

"(KEKKA to SHINKAZU no Wa) wo Miseru"

"To show the sum of KEKKA and SHINKAZU"

(displayln (+ KEKKA SHINKAZU ))

Example 2: Length of a list

This example shows the use of named functions, conditionals and recursion. I will use Haskell as pseudocode instead of Scheme.

長さとはカズ達と回数で
若しカズ達が空に等しいなら
回数ですけど、
そうでない、
【カズ達の尻尾】と【回数足す壱】の高さ
の事です。

丈とはカズ達で
カズ達と零の長さ
の事です。

本とは
カズ達は壱〜四十弐、
ナガサはカズ達の丈、
ナガサを見せる
の事です。

Let's start with the main program:

カズ達は壱〜四十弐、
"KAZUtachi ha ichi .. jonjuuni"

kazutachi = [1 .. 42]

We create a list kazutachi with the range operation 〜

ナガサはカズ達の丈、
"NAGASA ha KAZUtachi no Jou"

nagasa = jou kazutachi

We call the function jou on kazutachi and bind the result to nagasa

ナガサを見せる
"NAGASA wo Miseru"

print nagasa

The function jou is quite simple:

丈とはカズ達でカズ達と零の長さの事です。
"Jou ha KAZUtachi de KAZUtachi to Zero no Nagasa no Koto desu."

jou kazutachi = nagasa kazutachi 0

Finally nagasa

Nagasa ha KAZUtachi to KAISUU de
Moshi KAZUtachi ga Kuu ni Hitoshii nara
KAISUU desukedo,
soudenai,
(KAZUtachi no shippou) to (KAISUU Tasu Ichi) no Nagasa
no Koto desu

In Haskell:

nagasa kazutachi kaisuu = 
    if kazutachi == [] 
        then kaisuu 
        else nagasa (tail kazutachi) (kaisuu+1)

Example 3: Summing a list

This example shows the use of comments, named functions, conditionals, let-bindings and recursion. I will again use Haskell for the equivalent code.

註 再帰関数の例の「加える」。

加えるとは
カズ達とサムで
〈カズ達の長さ〉が零に等しい場合は
サムですけれど、
そうでない場合は、
このノコカズ達とシンサムを加えるに
カズ・ノコカズ達がカズ達、
シンサムがサムにカズを足す
の事です。

註 主プログラム。

本とは
コタエは[三十四と八]と零を加える、
コタエを見せる
の事です。

Starting again with the main program:

"KOTAE ha [sanjuuyon to hachi] to zero wo Kuwaeru"

kotae = kuwaeru [34,8] 0

Then the function 加える

Kuwaeru to ha
KAZUtachi to SAMU de
(KAZUtachi no Nagasa) ga zero ni hitoshii baai ha
SAMU desukeredo,
soudenai baai ha,
kono NOKOKAZUtachi to SHINSAMU wo Kuwaeru ni
KAZU:NOKOKAZUtachi ga KAZUtachi
SHINSAMU ga SAMU ni KAZU wo Tasu
no Koto desu.    

This function uses the 場合 variant of the if-then-else

〈カズ達の長さ〉が零に等しい場合は
サムですけれど、
そうでない場合は、
...

It also has a let-expression:

このノコカズ達とシンサムを加えるに
カズ・ノコカズ達がカズ達、
シンサムがサムにカズを足す

In Haskell this becomes:

kuwaeru kazutachi samu =
    if length kazutachi == 0 
        then samu 
        else
            let                
                kazu:nokokazutachi = kazutachi
                shinsamu = samu + kazu
            in
                kuwaeru nokokazutachi shinsamu

Language guide

  • Haku is a simple, mostly-pure, implicitly typed, strict functional language.
  • Think Scheme with a sprinkling of Haskell.
  • TODO: At the moment, all type checking is deferred to Raku, the implementation language. So the currently Haku is not really strict and more dynamically typed.

Punctuation

  • As Haku does not rely on whitespace, spaces and newlines are not delimiters.
  • Bindings in a let and expressions and bindings in the main program must be delimited by 、or 。.
  • At some places, newlines are allowed to ease readability.

All comments must start with 註 or 注 (so you can write 注意) and end with a 。. A newline is allowed after a comment.

Program structure

  • A haku program source file can contain named function definitions and must contain a main program, called 本.

  • The main program differs from the functions in that functions must be pure and therefore consist of a single expression, whereas the main program can be a sequence of expressions, similar to the do-sequence in a Haskell main program. The main program is defined as

      本とは 
      <var1>は<rhs-expression1>、
      <var2>は<rhs-expression2>、
      ...
      <expression1>、
      <expression2>、
      ...
      の事です。
    

There are a few variants to make it sound a bit more formal:

  • 本とは can also be 本真とは
  • の事です。can also be と言う事です。

A newline is allowed after 本とは and after all bindings and expressions.

Identifiers

  • variables: the first character must be katakana; further characters katakana or number kanji. The last character can be 達, to indicate a plural.
  • function names: must start with a kanji. If they are nouns, further characters are also kanji; if they are verbs, further characters are hiragana verb endings.

Constants

  • integer: written using number kanji. For zero, either 零, ゼロ or ◯. Negative number prefix is マイナス, optional positive number prefix is プラス
  • rational: two integers separated by 点
  • string: 「」or 『』
  • list: consist of identifiers or constants separated by と or 、

Named function definitions

In Haku, named function definitions are statements. The structure is

<function-name> とは <argument-list> で <expression> の事です。

The same closing variants as for 本 are allowed; a newline is allowed after とは, で and the expression.

Lambdas

或 <argument-list> で <expression>

Function application

There are a few forms of function application:

  • Verb form:

      <arg-list> を [ <arg-list> で ] <function>
    
  • Adjectival verb form (single argument only):

      <function> <arg>
    
  • Noun form:

      <arg-list> の [ 、 <arg-list> での ] <function>
    

The argument list can optionally be followed by の皆. This is used in particular when applying map or fold. Also, instead of で you can use のために or の為に.

Partial application

The arg list can be followed by だけ or 丈 to indicate partial application.

Map and Fold

Haku has built-in map and foldl:

foldl: 畳み込む

<list>と<accumulator>を<function>ので畳み込む

map: 写像する

<list>の皆を<function>ので写像する

Function composition

We use 後, 'のち', to compose functions:

<function1>後<function2>

Note that

f1後f2 

corresponds to

f2 . f1 

Currently, you have to bind them to a variable or wrap them in a lambda to apply them (TODO)

Let binding

There are two forms. The first is more like where-clause (expression at the start):

この <expression> に <variable> が <expression> 、... 。

A newline is allowed after この, に and every bind expression.

The second is more like a conventional let (expression at the end):

●<variable>は <expression>、
●...
では<expression>

A newline is allowed after every bind expression.

Conditional expressions

Similar to other Japanese natural programming languages, we use 若し or もし as the keyword to introduce an if-then-else. The condition can be either なら, ならば or 〜たら. The 'true' expression can optionally be followed by ですけれども or variants; the 'false' branch is introduced by そうでなければ or そうでないなら.

For example

もし <cond-expression> ならば <expression> そうでなければ <expression> 。

そうでなければ can also be written そうでない. A newline and/or comma is allowed after なら and before and after そうでなければ. Before そうでなければ, an optional ですけれど/ですけど/ですけれども or ですが is allowed.

Currently, there is another form of if-then-else expression supported, which uses 場合:

<cond-expression>場合は<expression>ですけれど、そうでない場合は<expression>

The ですけれど is optional and can also be ですけど, ですけれども or ですが; a newline and/or comma is allowed after 場合は and before そうでない.

Operators

Haku provides a minimal set of arithmetic and logical operations and numerical comparisons. Built-in operators in Haku can have a different syntax from ordinary function calls. There is no operator precedence handling, so combined expressions need parentheses.

Arithmetic

Verb form:

<expression> <verb> <expression>

or

<expression> と <expression> を <verb>

+: 足す
-: 引く
*: 掛ける
/: 割る

Noun form:

<expression> と <expression> の <noun>

+: 和
-: 差
*: 積
/: 除

Logical

TODO

Comparison

<expression> が <expression> <comparison-operation>

==: に等しい    
>: より多い
<: より少ない

TODO:

>=: 以上
<=: 以下

Lists

  • Haku list are simply expressions separated by と ( or に, から or まで), without parentheses.

  • Square brackets (角括弧) [] are used for nesting lists.

  • The empty list is 空.

  • Lists have a minimal set of list manipulation functions:

      length: <list>の長さ
      head: <list>の頭
      tail: <list>の尻尾
      cons:・(中黒) 
      concatenation: <list1>と<list2>を合わせる
      range operator: <integer>〜<integer> 
    

TODO:

reverse: を反転する or の逆引き     

Maps

  • Maps ("dictionaries") are created from lists of pairs,

      <key1>と<value1>と...で図を作る
    

    or from an empty list

      空で図を作る
    

    or shorter

      空図
    
  • Maps support the following functions:

      has: <map>に<key>が有る
      insert: <map>に<key>と<value>を入れる
      lookup: <map>に<key>を正引きする (TODO: 探索する)
      delete: <map>から<key>を消す
      length: <map>の長さ 
      keys:    <map>の鍵
      values:  <map>の値
    

System call

TODO:

機関で「ls」する

For interpolation: 《バリュー》; returns a string.

I/O

TODO:

  • Minimal file I/O for text files only.

      open: 
          <file>を<mode>の為に開ける
          where
          <mode>: 読む (read) or 書く (write)
          or
          <file>を開ける
          for read-write
    
      write: <string>を<filehandle>で書く
      read: 
          a single line: 一線を<filehandle>で読む、
          all lines: 全線を<filehandle>で読む、
    
      close: <filehandle>を閉める
    
      eof: <filehandle>の終了 (TODO)
    

Types

TODO

Modules and imports

TODO

Expressiveness

Haku tries to be more like a natural language. Apart from adopting Japanese writing and word order, it does this mainly in two ways:

Verb conjugation on function calls

Haku lets you conjugate the verbs for a function call. For example:

  • Given a function send:

      送るとは... 
    

    and an argument message:

      メッセージ  
    
  • In Scheme:

      (send message)
    
  • Plain Haku

      メッセージを送る。
      “To send a message”
    
  • Polite Haku

      メッセージを送って下さい。
      “Please send the message”
    
  • Insistent Haku

      メッセージを送なさい。
      “Do send the message”
    
  • Adverbial

      送ったメッセージ。
      “The sent message”
    

TODO: Not all of this works yet, currently you can do dictionary form (adjectival or plain), -te form with or without 下さい、しなさい and even くれて. And です can be で御座います.

Choice of list separators and function application constructs

There is a lot of choice in how to express certain constructs, in particular function application. For example,

書類を読むの為に開ける
"to open the document for reading"  
open doc ReadOnly

辞書にカギとバリューを入れる
"to insert a key and value in the dictionary"
insert dict key value

ジショにカギを正引きする
"to lookup a key in the dictionary"
lookup dict key

辞書からカギを消す
"to delete a key from the dictionary"
delete dict key

カギとバリューから図を作る 
"to create a map from key and value"
fromList [(key,value)]

カズ達とアクを足すので畳み込む
"to fold the numbers and the acc with addition"
foldl add acc xs

数達の皆を二倍で写像する
"to map all numbers with double"
map double xs

六に七を足す
"add seven to six"
6+7
六で掛ける七
"multiply seven with six"
6*7
六と七の積
"the product of six and seven"
6*7

I plan to add support for adjectives as well (TODO).

Motivation

The Wikipedia page on Non-English-based programming languages lists eight different languages based on Japanese. So why make a ninth one? The short answer is, to see what I would end up with. The slightly longer answer is that these other eight languages serve a practical purpose: they want to make programming easier for Japanese native speakers, and most of them target education.

My motivation to create Haku is very different. I don't want to create a practical language. I want to explore what the result is of creating a programming language based on a non-English language, in terms of syntax, grammar and vocabulary. In particular, I want to allow the programmer to control the register of the language to some extent (informal/polite/formal).

Syntax

I also want the language to be closer, at lease visually, to literary Japanese. Therefore Haku does not use Roman letters, Arabic digits or common arithmetic, logical and comparison operators. And it supports top-to-bottom, right-to-left writing.

Grammar

The main motivation for Haku is the difference in grammar between Japanese and most Indo-European languages. In particular, it has subject-object-verb order. This makes the familiar programming constructs quite different.

Some time ago I ran a poll about how coders perceive function calls, and 3/4 of respondents answered "imperative" (other options were infinitive, noun, -ing form).

In Japanese, the imperative (命令形, "command form") is rarely used. Therefore in Haku you can't use this form. Instead, you can use the plain form, -masu form or -te form, including -te kudasai. Whether a function is perceived as a verb or a noun is up to you, and the difference is clear from the syntax. If it is a noun, you can turn it into a verb by adding suru, and if it is a verb, you can add the 'no' or 'koto' nominalisers. And you can conjugate the verb forms.

Naming and giving meaning

In principle, programming language does not need to be based on natural language at all. The notorious example is APL, which uses symbols for everything. Agda programmers also tends to use lots of mathematical symbols. It works because they are very familiar with those symbols. An interesting question is if an experienced programmer who does not know Japanese could understand a Haku program; or if not, what the minimal changes would be to make it understandable.

To allow to investigate that question, the Scheme and Raku emitters for Haku supports (limited) transliteration to Romaji. And there is also Roku, but more about that later ...

Parsing

Japanese does not use spaces. So how do we tokenise a string of Japanese?

  • There are three writing systems: katakana (angular), hiragana (squigly) and kanji (complicated).
  • Katakan is used in a similar way as italics
  • Nouns, verb, adjectives and adverbs normally start with a kanji
  • Hiragana is used for verb/adjective/adverb endings and "particles", small words or suffixes that help identify the words in a sentence.
  • A verb/adjective/adverb can't end with a hiragana character that represents a particle.

So we have some simple tokenisation rules:

  • a sequence of katakana
  • a kanji followed by more kanji or hiragana that do not represent particles
  • hiragana that represent particles

Where that fails, we can introduce parentheses. In practice, only specific adverbs and adjectives are used in Haku. For example:

ラムダ|は|或|エクス|で|エクス|掛ける|エクス|です

ラムダ: katakana word
は: particle
或: pre-noun adjective
エクス: katakana word
で: particle
エクス: katakana word
掛ける: verb
エクス: katakana word 
です: verb (copula)

Implementation

Haku is implemented in Raku, a gradually-typed multi-paradigm language.
The parser uses Raku’s Grammars. It is a recursive descent, longest token match parser. The parser populates an AST using Haku’s Actions. The AST is an algebraic datatype implemented using Haku’s Parameterized Roles. The emitter generates Raku code which is executed via dynamic module loading. Currently all type checking is delegated to Raku.

About the name

I call it 'haku' because I like the sound of it, and also because that word can be written in many ways and mean many things in Japanese. I was definitely thinking about Haku from Spirited Away. Also, I like the resemblance with Raku, the implementation language. I would write it 珀 (amber) or 魄 (soul, spirit).



from Hacker News https://ift.tt/3AzXKFQ

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.