基本

予約語

予約語は次の6つしかありません。

do, if, else, in, open, load

コメント

C#/C++/Java と同じ方式です。

// コメントです
/*
ほげほげ
ぴよぴよ
*/

リテラル

123     // 整数値
3.14    // 浮動小数点数
"hello" // 文字列
true    // 
false   // 
()      // null
(a, b)  // タプル
[a, b]  // 配列
~[a, b]~ // 閉区間
~(a, b)~ // 開区間
~[a, b)~ // 半開区間

数学と似たような形式で書ける区間リテラルが特徴的です。

変数束縛

a := 1;
b := 3.14;
c := "hello";
...

行末にセミコロンがついているので C/C++/C#/Java 等に親しんでいるプログラマにとっては「文」のように見えると思いますが、FunnyScript は式のみで構成される文法であり文は存在しません。このセミコロンは F# や OCaml などの in に相当する役割です。

条件分岐

result := if (x % 2 == 0) "even" else "odd";

たいていの言語の if 式とちょっと違うのは、else if の else を省略できることです:

fizzbuzz := n ->
  if (n % 3 == 0 && n % 5 == 0) "fizzbuzz"
  if (n % 3 == 0) "fizz"
  if (n % 5 == 0) "buzz"
  else n.ToString();

mutable 変数と代入

mutable という組み込み関数を用いると、<- 演算子によって代入(値の書き換え)が可能なオブジェクトが生成できます:

a := mutable 1; // mutable 関数でミュータブル変数を生成
do a <- 2;      // <- 演算子で代入

演算子

設計ポリシー

演算子を設計するに当たり、2つの指針を立てました。

  • 言語によって混乱があるので、 思い切って = 演算子は使わないことにする
  • 概ね C#/Java/C/C++ プログラマにとって違和感が少ない演算子にする

= 演算子はありません

= という演算子は、言語によって「変数定義」「代入」「同値判定」など異なる用途に使われており、しばしば混乱を招きます。 そこで FunnyScript では = という演算子は使わないことにしました。

  • 変数束縛は := で行います。
  • 代入は <- で行います。
  • 同値判定は == で行います。

C#/Java/C/C++ と似たような演算子体系

  • 同値判定は ==, !=
  • 比較は <, >, <=, >=
  • 否定は !
  • 論理演算は &&, ||
  • 四則演算等は +, -, *, /, %

※ ビット演算子は(まだ)定義されていません。

パイプライン演算子

F# と同様の作用を持つパイプライン演算子 |> が定義されています。

sub := x -> y -> x - y;
a := sub 10 3;    // 10 - 3 = 7
b := 3 |> sub 10; // 10 - 3 = 7

上記で定義されている sub はカリー化された引数を2つもつ関数です。関数定義については後述します。

関数

ラムダ式

ラムダ式は 引数 -> の形式で定義できます:

add10 := x -> x + 10; // x に10を足す関数
a := add10 20;  // 呼び出し

add := x -> y -> x + y; // x + y を計算する関数(カリー化)
a := add 10 20;  // 呼び出し

add := (x, y) -> x + y; // x + y を計算する関数(タプル)
a := add (10, 20);  // 呼び出し

hello := () -> System.Console.WriteLine "Hello World"  // 引数なしの関数
do hello ();  // 呼び出し

ラムダ式以外に関数を定義する文法はありません。

ラムダ式の利用例として、配列の要素をラムダ式で map するコードを示します。 (このコードにはまだ説明していない map 関数が出てきていますが、雰囲気で読んで下さい。):

[1, 2, 3] |> map (x -> 2 * x) // [2, 4, 6]

簡易ラムダ式

上記のラムダ式も十分簡潔ですが、1変数関数に限っては更に簡潔に書ける文法も用意しました:

add10 := x -> x + 10; // x に10を足す関数
add10 := | @ + 10; // こう書いても同じです
  • | が簡易ラムダ式の開始の記号です。
  • @ が暗黙的に定義された引数です。

これを使うと、先ほどの配列を map するコードは次のように書けます:

[1, 2, 3] |> map (| 2 * @) // [2, 4, 6]

引数に特に意味のある名前が不要な場合に、この簡易記法が使えます。

再帰呼び出し

次のように再帰呼び出しができます:

fac := rec| n -> if (n == 0) 1 else n * @ (n - 1);   // 階乗計算

rec は組み込み関数として定義されたYコンビネータです。関数定義の前に rec| を書いておくと、 @ で自分自身を呼び出すことが出来るようになります。

このコードは下記コードと等価です:

fac := rec f -> n -> if (n == 0) 1 else n * f (n - 1);       // 階乗計算

つまり rec|| は簡易ラムダ式を表すものであり、 @ は簡易ラムダ式の引数です。前節の簡易ラムダ式の説明と合わせてコードを見比べてみてください。

なお、 rec は次のように定義されています:

rec :=
  Y := mutable ();
  do Y <- f -> x -> f (Y f) x;
  fix Y;

末尾再帰最適化

出来ます。

実装としては、F# の末尾再帰最適化が効くような書き方にして後はF#コンパイラにお任せしている感じです。 次のようなコードが Stack Overflow することなく延々と動きます:

iter := rec| n -> do System.Console.WriteLine n; @ (n + 1);
do iter 0;

メンバ参照ラムダ式

後述するように、FunnyScript では .NET Framework で定義されているクラスのインスタンスを生成することができますし、レコードを定義する文法もあります。 たいていの言語と同様に、. 演算子によってそれらのメンバーを参照することができます。

しばしば、引数でそういったオブジェクトを受け取ってそのオブジェクトのメンバーを呼び出す関数が必要になることがあります:

// x を受け取って x のメンバーを呼び出す関数
f := x -> x.member arg1 arg2;

これを次のように簡潔に定義できるシンタックスシュガーを用意しました:

f := .member arg1 arg2;

これは次のようなメソッドチェインで効果を発揮します:

s :=
  System.Text.StringBuilder()
  |> .Append "Hello "
  |> .Append "World, "
  |> .Append "Hello "
  |> .Append "FunnyScript!"
  |> .ToString();

次の3行はすべて同等のコードです:

["hello", "world"] |> map (s -> s.ToUpper())
["hello", "world"] |> map (| @.ToUpper()) // 簡易ラムダ式記法
["hello", "world"] |> map (.ToUpper()) // メンバ参照ラムダ式

データ構造

レコード

次のように中括弧 { } で囲むことでレコードオブジェクトが作れます:

a := { name := "u1roh"; age := 40; };
do System.Console.WriteLine ("name = {0}, age = {1}", a.name, a.age);

この応用でモジュール(関数などをグループ化したもの)も作れます:

Calc := {
  add := x -> y -> x + y;
  sub := x -> y -> x - y;
};
do System.Console.WriteLine ("add: {0}", Calc.add 7 9);
do System.Console.WriteLine ("sub: {0}", Calc.sub 8 2);

つまり FunnyScript はレコードとモジュールに文法の違いはありません。 単に { } によってグループ化出来る機能を提供するだけであり、それをレコードとして使うのもモジュールとして使うのもユーザーの自由です。

次のように書くと mutable なメンバーを持つレコードが作れます:

// ミュータブルなメンバを持つレコード
hoge := { piyo := mutable 1; };
do hoge.piyo <- 2;

配列

中身は普通の .NET の配列オブジェクトです:

a := [1, 2, 3];  // リテラル
len := a.Length; // 3
type := a.GetType();  // System.Object[]
b := array 3 (i -> 2 * i);  // [0, 2, 4]
a1 := a 1;  // 2(インデックスによる要素取得)
c := a + b; // [1, 2, 3, 0, 2, 4] (配列の連結)

特にインデックスによる要素取得は特徴的です。 カギ括弧の演算子を使って a[1] のように表記する言語のほうが一般的かもしれません。 しかし FunnyScript ではカギ括弧を使わずに単に a 1 と表記します。 これは関数適用と同じ表記です。 つまり、配列は「インデックス(整数)を引数に与えると要素を返す関数」として扱うことが出来ます。

例として、次のコードは countries がラムダ式として map の引数に与えられています:

countries := ["Japan", "China", "USA", "Russia"];
[2, 0, 3] |> map countries // ["USA", "Japan", "Russia"]

なお、関数型言語によくある単方向連結リストの機能は用意していません。

組み込み関数

foreach

do [1, 3, 5, 7] |> foreach System.Console.WriteLine;

.NET Framework の呼び出し

関数呼び出し

既にサンプル中に System.Console.WriteLine の呼び出しは登場しています。 同様に他の関数も呼び出せます:

a := System.Math.Abs (-1); // 1
b := System.Math.Sin 3.14; // 0.00159...

namespace の open

open System;
a := Math.Abs (-1); // 1
b := Math.Sin 3.14; // 0.00159...

クラスのインスタンス生成

open System.Collections;
stack := Stack();
do stack.Push 123;
do stack.Push 456;
n := stack.Count;

クラスがそのままコンストラクタとして扱われます。 クラスにコンストラクタ引数(上記の場合は ())を渡すとインスタンスが生成されます。

もちろん、次のようにコンストラクタに () ではない引数を渡すことも可能です:

s := System.Text.StringBuilder "Foo";
do s.Append "Bar";
do s.Append "Buzz";

ジェネリッククラスのインスタンス生成

open System;
open System.Collections.Generic;

a := Stack Int32(); // Stack<int> の生成
do a.Push 12;

b := Dictionary (String, Double) (); // Dictionary<string, double> の生成
do b.Add ("PI", 3.14159);
do b.Add ("e", 2.718);

次のような形式でインスタンスが生成されていることがわかります:

ジェネリッククラス 型引数 コンストラクタ引数

つまり、ジェネリッククラスは次のようにカリー化された関数であると考えることが出来ます:

ジェネリッククラス : 型引数 -> コンストラクタ引数 -> インスタンス

プロパティの代入

// CLRオブジェクトのプロパティの代入も出来ます
timer := System.Timers.Timer();
do timer.Enabled <- true;

エラー処理

正直なところ、エラーの扱いはまだ仕様が整理しきれていません。 現時点でどうなっているかを簡単に述べます。

error 関数でエラーが発生します:

error "error message"

パターンマッチ

match 関数

match という組み込み関数でパターンマッチによる分岐が出来ます。 次のサンプルコードを順に説明していきます:

f := match [
  (x, y) -> x + y, // ..................... (A)
  { fizz := x; buzz := y } -> x * y, // ... (B)
  (x: System.Double) -> 3.14 * x, // ...... (C)
  n -> if (n % 2 == 0) n / 2, // .......... (D)
  | if (@ % 3 == 0) @ / 3, // ............. (E)
  | @ // .................................. (F)
];

[ (1, 2), // A にマッチ
  { fizz := 2; buzz := 3 }, // B にマッチ
  2.0, // C にマッチ
  6,   // D にマッチ
  21,  // E にマッチ
  13   // F にマッチ
] |> map f

結果:

[3, 6, 6.28, 3, 7, 13]
  • match というのは構文上のキーワードではなく関数として実現されています。 match は引数を2つ取る(カリー化された)関数です。
  • サンプルコードで最初に定義されている f は、match [...] という形をしています。 つまり、match に引数を1つだけ部分適用した形になっているので、f は引数を1つ取る関数となります。
  • 結果を返す式は [...] |> map f という形をしています。 さまざまな形(パターン)のデータを配列で用意し、引数として f に入力した結果を返しています。

クラス定義

class 関数を使ってクラスを定義することができます:

Person := class ((first, last) -> { first_name := first; last_name := last; }) {
  fullname := self ->
     System.Text.StringBuilder ()
     |> .Append self.first_name
     |> .Append " "
     |> .Append self.last_name
     |> .ToString();
};
charlie := Person ("Charlie", "Parker");
do System.Console.WriteLine charlie.fullname;

class 関数は引数を2つ取ります。

第1引数はコンストラクタです。 上のコードではファーストネームとラストネームをタプルで受け取り、レコードオブジェクトを生成するコンストラクタとなっています。 なお、コンストラクタに取れる引数は1つだけです。複数の引数が必要な場合はタプルで受け取るようにします。

第2引数はメンバーテーブルとなるレコードオブジェクトです。 メンバーは必ず関数とし、その第1引数には自分自身(コンストラクタで生成したもの)を受け取ります。 上のコードでは self という名前で受け取っていますが、特に self は予約語ではなく名前は何でもよいです。

「継承」はできません。実装する予定もありません。

「簡易ラムダ式」を使うとメソッドは下記のように書くことが出来ます:

Person := class ((first, last) -> { first_name := first; last_name := last; }) {
  fullname := |
     System.Text.StringBuilder ()
     |> .Append @.first_name
     |> .Append " "
     |> .Append @.last_name
     |> .ToString();
};