🤖

自作プログラミング言語を初級者が1週間で作る方法 (5) 関数

2022/12/30に公開

自作プログラミング言語をなるべく簡単に作る方法を紹介します。また、実際に自作プログラミング言語を作ってみます。

前回の記事の続きです。今回は関数を使えるようにします。

ラムダ式

今回は関数の文法を作ってみます。関数とは、ある入力を与えたときに一つの出力を返すものです。たとえば

  • f(x) = 3 x + 5

という関数は、入力 x を与えたときに出力 3 x + 5 を返します。

最近のプログラミング言語は関数をラムダ式で書けるようにしていることが多いです。そこでこの記事ではラムダ式の文法を作ってみます。ラムダ式は input -> output のような形で書くことが多いです。先ほどの関数をラムダ式で書くと

x -> 3 * x + 5

となります。あるいは

\x -> 3 * x + 5

のように書くこともあります。\ は、一文字目を見てすぐにラムダ式だとわかるようにするための飾りです。\ ではなく funfn などを付けることもあります。Yuz 言語では \ を付けることにします。何も付けない文法にしても構文解析は可能です。

JavaScript のラムダ式には飾りはありません。変換後の JavaScript コードは

x => 3 * x + 5

となります。

ラムダ式を作る

ラムダ式の定義は、前回の記事Expression のところに書き加えて if 式と同列とします。

Expression
  = LambdaExpression
  / IfThenElseExpression
  / OrExpression

LambdaExpression
  = "\\" _ i:Identifier _ "->" _ e:Expression {
      return `${i} => ${e}`;
    }

IfThenElseExpression
  = "if" __ a:Expression __ "then" __ b:Expression __ "else" __ c:Expression {
      return `${a} ? ${b} : ${c}`;
    }

OrExpression
  = ...

LambdaExpression の定義の中にもう一度 Expression を入れました。このような循環した定義にするとラムダ式の中にラムダ式を入れられるようになります。たとえば

\x -> \y -> x + y

と書いたときには

\x -> (\y -> x + y)

と同じ意味になります。これは関数を返す関数になっていますが、次節で紹介する方法を用いることで二変数関数の

  • f(x, y) = x + y

として使うことができます。三変数関数も同様にして

\x -> \y -> \z -> x + y + z

のように作ることができます。このように一変数関数をネストして作った疑似的な多変数関数のことをカリー化関数といいます。Yuz 言語ではカリー化関数を用いることにします。

関数適用

関数を作れるようになったので次は関数を使えるようにします。よくある方法は

  1. ラムダ式で関数を作る
  2. それを変数に束縛する
  3. 関数適用の () を作用させる

というものです。次のような方法です。

let twice = \x -> x * 2;
twice(3)

このコードは 6 を返します。

しかしこの書き方だと計算の優先順位を表す () と関数適用を表す () が同じ記号になってしまって紛らわしいです。ここでは文法をわかりやすくするために関数適用に [] を使うことにします。つまり

let twice = \x -> x * 2;
twice[3]

とします。多変数関数についてはカリー化の性質から

let add = \x -> \y -> \z -> x + y + z;
add[1][2][3]

のようになります。これは x1 を、y2 を、z3 を適用したことになります。また、途中までしか関数適用しないこともできて

let add = \x -> \y -> \z -> x + y + z;
let add12 = add[1][2];
add12[3]

といったことができます。add[1][2] のように途中までしか関数適用しないことを関数の部分適用といいます。add12 に束縛される関数は \z -> 3 + z です。

この関数適用の文法を作ると次のようになります。関数適用は二項演算子よりも優先順位を高くしたいので、前回の記事MultExpressionPrimary の間に入れます。

MultExpression
  = head:CallExpression tail:(_ MultOperator _ CallExpression)* {
      return tail.reduce((acc, x) => `(${acc}) ${x[1]} (${x[3]})`, head);
    }

MultOperator
  = "*"
  / "/"
  / "%"

CallExpression
  = head:Primary tail:(_ "[" _ Expression _ "]")* {
      return tail.reduce((acc, x) => `${acc}(${x[3]})`, head);
    }

Primary
  = Paren
  / Float
  / Boolean
  / Identifier
  • (\x -> x + 4)[3] は正しいプログラムです。
  • (\x -> x + 4)[1][2]3.14[3]true[5] は実行時エラーとなります。
  • 10 + [5] はパースエラーとなります。

実行例

最後に実行例を示します。ラムダ式の文法を \x -> x + 1 のようにしましたが、\ がなくても構文解析は可能です。両方の文法をそれぞれ作って実行してみると次のようになります。

LambdaExpression
  = "\\" _ i:Identifier _ "->" _ e:Expression {
      return `${i} => ${e}`;
    }

Input: let add = \x -> \y -> \z -> x + y + z; let add12 = add[1][2]; add12[3], Output: 6

LambdaExpression
  = i:Identifier _ "->" _ e:Expression {
      return `${i} => ${e}`;
    }

Input: let add = x -> y -> z -> x + y + z; let add12 = add[1][2]; add12[3], Output: 6

まとめ

今回は関数を使えるようにしました。次回はレコード(構造体、連想配列)と中置関数を使えるようにします。

Discussion