63
59

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

C#/JavaScriptで学ぶF#入門

Last updated at Posted at 2017-01-04

C#やJavaScript(ES2015)と比較しながらF#の文法を説明します。手続型の延長線上で取っ掛かりをつかむことを目的とします。関数型については深追いしません。

  • とりあえず手続型的な発想でも構わないので、F#を使ってみます。
  • 関数型特有の概念の説明には重点を置きませんが、その導入になるようには意識します。
  • 一気に関数型に飛ばないで、ベターC#として慣れていくような入り方を目指します。
  • 関数型の理解を深めるのは慣れてからでも遅くないというスタンスです。

この記事は以前開催していたF#入門のテキストを改訂したものです。

この記事には姉妹編があります。

F#を手っ取り早く試すために、私が常用している環境を紹介します。

F#について

F#の構文は見慣れないものだと思います。この記事ではC#/JavaScriptと比較しながら構文に慣れることに重点を置きます。

背景

F#が分かりにくいと感じる原因は、主に以下の2種類ではないでしょうか。

  1. 構文の異質さ(C系言語などと比較して)
  2. 関数型の考え方

今回は前者の壁に的を絞ります。両者は完全に分離しているわけではないため、後者の領域も多少は言及します。個人的にはF#は構文が簡潔で短く書けるのが良いと思っています。触り始めの頃は関数型のことはあまり意識しませんでした。

位置付け

F#はC#と同様に.NET Frameworkで動く言語です。クラスを定義したり使ったりなど、基本的にはC#でできるのとほぼ同じことができます。

※ これは大雑把な説明で、細かい点で違いはあります(protectedや入れ子にされた型など)。

それに対してJavaScriptは出自が異なりますが、C#よりもJavaScriptで説明した方が分かりやすいケースもあるため、説明に取り入れました。

ハローワールド

※ 横幅の関係上、インデントはスペース2つとします。

C# JavaScript F#

using System;
class Program
{
static void Main()
{
Console.WriteLine("hello");
}
}

console.log("hello");

printfn "hello"
または

open System
Console.WriteLine "hello"
  • C#はMainメソッドが必須で、クラスで包む必要があります。簡単のため以後の例では省略します。
  • JavaScriptとF#はクラスやMainで包む必要がありません。
  • F#はセミコロンが必要ありません。(JavaScriptも省略可能)
  • F#では引数を()で囲む必要がありません。(付けても動きます)
  • F#のopenはC#のusingに相当します。以後の例では省略します。

printf/printfn

F#のprintf/printfnopenも何もなくいきなり使える標準の出力用関数です。

F#
printf "hello\n"
printfn "hello"

関数名の接尾辞n"\n"と同様に New line に由来して、改行を意味します。

複数の引数はコンマではなくスペースで区切ります。(詳細は次のセクションで解説します)

F#
printfn "%d" 1

printfはコンパイラがフォーマット文字列を認識して、引数の型をチェックします。

F#
printfn "%d" "abc"  // エラー
printfn "%s" 0      // エラー

複数の引数

F#では複数の引数の扱い方が二系統あり、F#ネイティブの関数と.NETのメソッドとで異なります。

C# JavaScript F#

Console.WriteLine("{0} {1}”,
  1 + 1, Math.Sqrt(2));

console.log(1 + 1, Math.sqrt(2));

printfn "%d %f" (1 + 1) (sqrt 2.)
または

Console.WriteLine("{0} {1}",
  1 + 1, Math.Sqrt 2.)

※ F#の2.は2.0のことで、浮動小数点数であることを示します。(後述)

F#ネイティブの関数

複数の引数はコンマではなくスペースで区切ります。

F#
printfn "%d" 1

コンマを付けると警告されてうまく動きません。(後述のタプルとして扱われます)

F#
printfn "%d", 1  // 警告され文字も出力されない

括弧で囲む必要はありません。付けるとエラーになります。

F#
printfn("%d" 1)   // エラー
printfn("%d", 1)  // エラー

引数で計算や関数呼び出しを行う場合、引数としてまとめるため括弧で囲む必要があります。

F#(再掲)
printfn "%d %f" (1 + 1) (sqrt 2.)

.NETのメソッド

タプルと呼ばれる複数の値を組み合わせた型として扱われます。C#と同じスタイルで、引数はコンマで区切って括弧で囲みます。

F#
Console.WriteLine("{0}", 1)

括弧を省略すると警告されてうまく動きません。

F#
Console.WriteLine "{0}", 1  // 警告され実行時例外

.NETのメソッドでも1引数の場合は括弧が省略可能です。

F#
Console.WriteLine "hello"

以後の例では、F#はネイティブの関数(printfnなど)があれば、.NETのメソッド(Console.WriteLineなど)よりも優先して使用します。

数値型のキャスト

F#では数値型を自動的にキャストしてくれないためsqrt(2)はエラーになります。
C#/JavaScriptから見ると不親切ですが、型推論を優先するための言語設計です。

浮動小数点数型はC#とは型名が異なるため注意が必要です。

C# F#
float float32
double float

倍精度が浮動小数点数の基本で、単精度がオプショナルという解釈だと思われます。

おまけ

LISPを知っていれば、最上位の括弧が省略されていると考えればしっくり来るかもしれません。括弧で囲んでも動きます。

F#
(printfn "%f" (sqrt 2.))

変数

F#の変数はデフォルトで再代入できないという特徴があります。そのことについての説明は後に回して、まずは単純に変数を定義するケースを取り上げます。

変数はletで定義します。型推論されるため、C#のvarに相当します。JavaScriptではvarは関数スコープとなるため、ES2015で追加されたブロックスコープのletに相当します。

C# JavaScript F#

var a = 1;

let a = 1;

let a = 1

型を明示的に指定する方法もあります。JavaScriptはasm.jsで型で示します。

C# JavaScript F#

int a = 1;

let a = 1 | 0;

let a: int = 1

F#では可能な限り型推論に任せるスタイルを推奨します。

束縛

F#はデフォルトでは変数に入れた値は変更できません。変数と値の結び付きが強く、「代入」ではなく「束縛」と表現します。

C# JavaScript F#

const int a = 1;
Console.WriteLine(a);
a = 2;  // エラー

const a = 1;
console.log(a);
a = 2;  // エラー

let a = 1
printfn "%d" a
a = 2  // 警告(代入ではなく比較)

F#では===を書き分けずにどちらも=です。let以外の=は比較として扱われます。

F#で値が変更できるようにするにはmutableを付けます。値の変更には<-を使います。

C# JavaScript F#

int a = 1;
Console.WriteLine(a);
a = 2;
Console.WriteLine(a);

let a = 1;
console.log(a);
a = 2;
console.log(a);

let mutable a = 1
printfn "%d" a
a <- 2
printfn "%d" a

mutableを付けずに宣言した変数に再代入する方法はありません。

複数の変数定義

複数の変数を一度に定義する書式を示します。

C# JavaScript F#

int x = 1, y = 2;

let x = 1, y = 2;

let x, y = 1, 2

※ C#ではvarを使うと複数の変数を一度に定義できません。

F#の書式は括弧で囲めば数学に近くなり、座標の表記に似たものだと理解できます。括弧の有無で意味は変わりません。

F#
let (x, y) = (1, 2)

F# Interactive

ちょっとした実験は対話的に実行した方が便利です。F# Interactive (fsi) と呼ばれるREPLがあります。

Windows では fsi.exe ですが、Mono 環境では fsharpi コマンドで呼び出します。

fsiではセミコロンを2つ付けると、評価されて値が表示されます。

fsi
> let a=1;;
val a : int = 1

valは値(value)の意味です。

変数の値を見るには変数名だけでOKです。

fsi
> a;;
val it : int = 1

itは直前に評価された値が束縛されている変数で「それ(it)」の意味です。

fsi
> it;;
val it : int = 1

;;を付け忘れると複数行入力として扱われます。次の行で付ければ評価されます。

fsi
> a
- ;;

電卓としても使えます。

fsi
> 1+1;;
val it : int = 2

letなしの=が比較になっているのを確認します。

fsi
> a=1;;
val it : bool = true
> a=2;;
val it : bool = false

等しくないのを表すのは<>です。

fsi
> a<>1;;
val it : bool = false

終了は#q;;と入力します。

fsi
> #q;;

条件式

ifは構文が少し違うだけで基本的に同じです。

C# JavaScript F#

if (a == 1)
  Console.WriteLine("1");
else
  Console.WriteLine("?");

if (a == 1)
  console.log("1");
else
  console.log("?");

if a = 1 then
  printfn "1"
else
  printfn "?"

ブロック

F#はインデントでブロックが構成されるので、C#のように複文での中括弧に相当するものはありません。

C# JavaScript F#

if (a == 1)
{
  Console.WriteLine("1");
  Console.WriteLine("!");
}

if (a == 1) {
  console.log("1");
  console.log("!");
}

if a = 1 then
  printfn "1"
  printfn "!"

三項演算子

F#のifはそのまま三項演算子としても使えます。(ifは文ではなく式のため)

C# JavaScript F#

var b = a == 1 ? 2 : 0;

let b = a == 1 ? 2 : 0;

let b = if a = 1 then 2 else 0

複合技

F#では最後に評価された値が返されるため、処理と代入を混ぜることができます。これは慣れると便利な技です。

C# JavaScript F#

int b;
if (a == 1)
{
  Console.WriteLine("1");
  b = 2;
}
else
{
  Console.WriteLine("?");
  b = 0;
}

let b;
if (a == 1) {
  console.log("1");
  b = 2;
} else {
  console.log("?");
  b = 0;
}

let b =
  if a = 1 then
    printfn "1"
    2
  else
    printfn "?"
    0

タプル

条件分岐の結果、複数の値を代入するような処理を一気に書けます。うまくハマるととても簡潔になります。

C# JavaScript F#

int x, y;
if (a == 1)
{
  x = 1;
  y = 2;
}
else
{
  x = 3;
  y = 4;
}

let x, y;
if (a == 1) {
  x = 1;
  y = 2;
} else {
  x = 3;
  y = 4;
}

let x, y = if a = 1 then 1, 2 else 3, 4

この構文が分かりにくければ、括弧を付けて考えると良いかもしれません。

F#
let (x, y) = if a = 1 then (1, 2) else (3, 4)

関数

説明の都合上、F#は冗長な構文から先に紹介します。

C# JavaScript F#

class Test
{
  static int inc(int x)
  {
    return x + 1;
  }
  static int add(int x, int y)
  {
    return x + y;
  }
  static void Main()
  {
    Console.WriteLine(inc(1));
    Console.WriteLine(add(1, 2));
  }
}

function inc(x) {
  return x + 1;
}
function add(x, y) {
  return x + y;
}
console.log(inc(1));
console.log(add(1, 2));

let inc = fun x -> x + 1
let add = fun x y -> x + y
printfn "%d" (inc 1)
printfn "%d" (add 1 2)

F#は変数の束縛と同じ構文で、関数が束縛されています。

F#(対比)
let inc = 0
let inc = fun x -> x + 1

右辺の fun x -> x + 1 は左辺に束縛されて名前が付くことから、単体では名前が無く、無名関数などと呼ばれます。

ラムダ式

F#の書き方は、C#のラムダ式やJavaScriptのアロー関数式に相当します。

C# JavaScript F#

Func<int, int> inc = x => x + 1;
Func<int, int, int> add = (x, y) => x + y;
Console.WriteLine(inc(1));
Console.WriteLine(add(1, 2));

let inc = x => x + 1;
let add = (x, y) => x + y;
console.log(inc(1));
console.log(add(1, 2));

let inc = fun x -> x + 1
let add = fun x y -> x + y
printfn "%d" (inc 1)
printfn "%d" (add 1 2)

C#のFuncは冗長ですが、型推論が効かないため省略できません。

C#
var inc = (int x) => x + 1;  // エラー

ちなみにVB.NETでは匿名デリゲート型に型推論されます。

VB.NET
Dim inc = Function(x%) x + 1

※ C#では引数や戻り値の型が同じデリゲート間でもキャストできませんが、VB.NETではできることから、匿名デリゲート型に割り当てても問題がないという判断だと思われます。

糖衣構文

funを省略して引数を左辺に記述できます。通常はこちらを使います。

F# (funあり) F# (funなし)

let inc = fun x -> x + 1
let add = fun x y -> x + y

let inc x = x + 1
let add x y = x + y

funなしの方が便利です。最初に見せなかったのは、関数が値と同じように束縛されていることを示したかったためです。

fsi

F# Interactiveで色々な書き方を動作確認します。この手の簡単な確認にREPLは便利です。

F# Interactive JavaScript (Node.js)

let inc = fun x -> x + 1;;
inc 1;;
(inc 1);;
inc(1);;
(fun x -> x + 1) 1;;

inc = x => x + 1
inc(1)
(x => x + 1)(1)

※ 最後の例は関数を束縛せずにインラインで使っています。funを省略した構文では表現できません。

unit

C#でのvoidに相当するのがunitです。値としては()と表現します。

C# JavaScript F#

void test1() {}
int test2() { return 1; }

function test1() {}
function test2() { return 1; }

let test1() = ()
let test2() = 1

ラムダ式などで書いてみます。

C# JavaScript F#

Action test1 = () => {};
Func<int> test2 = () => 1;

let test1 = () => {};
let test2 = () => 1;

let test1 = fun () -> ()
let test2 = fun () -> 1

変数(test3)と関数(test4)を比べてみます。

C# JavaScript F#

int test3 = 0;
Func<int> test4 = () => 0;

let test3 = 0;
let test4 = () => 0;

let test3 = 0
let test4() = 0

ignore

F#では関数の戻り値を捨てると警告されます。

C# JavaScript F#

Func<int> a = () => 1;
a();  // 警告なし

let a = () => 1;
a();  // 警告なし

let a() = 1
a()  // 警告

警告を抑えるため、ignore関数で明示的に無視します。C言語でvoidにキャストする流儀に似ています。

C言語 F#

int a() { return 1; }
void test() { (void)a(); }

let a() = 1
ignore(a())

※ gccには戻り値を無視したときに警告する __attribute__((warn_unused_result)) があります。

パイプライン演算子

引数と関数を分離するパイプライン演算子というものがあります。ネストした引数の括弧を外してフラットに記述するのに使います。戻り値の警告を受けてignoreを追加するときに便利です。

F# F# (右向き) F# (左向き)

ignore(a())

a() |> ignore

ignore <| a()

右向きのパイプライン演算子は、シェルのパイプのような感覚で関数の多重呼び出しに使えます。左向きはHaskellの$に似ていますが、連続して使うと$とは意味が変わるため(後述)、連続させるときは<<演算子による関数合成と併用します。

F# F# (右向き) F# (左向き)

foo(bar(baz()))

() |> baz |> bar |> foo

foo << bar << baz <| ()

<|を連続して使用すると、複数の引数を個別に適用する意味となります。

F#
printfn "%d,%s" <| 5 <| "abc"

再帰関数

C#/JavaScriptでラムダ式を使わずに再帰で階乗を求めます。

C# JavaScript

class Test
{
  static int frac(int x)
  {
    return x < 1 ? 1 : x * frac(x - 1);
  }
  static void Main()
  {
    Console.WriteLine(frac(5));
  }
}

function frac(x) {
  return x < 1 ? 1 : x * frac(x - 1);
}
console.log(frac(5));

これをラムダ式で書きます。C#では自分自身が参照できなくなるため、一度nullで初期化するという小手先の技が必要となります。JavaScriptは参照が動的に処理されるため問題ありません。F#では自分自身を参照するために専用のrecキーワードが用意されています。

C# JavaScript F#

Func<int, int> frac = null;
frac = x =>
  x < 1 ? 1 : x * frac(x - 1);
Console.WriteLine(frac(5));

let frac = x =>
  x < 1 ? 1 : x * frac(x - 1);
console.log(frac(5));

let rec frac x =
  if x < 1 then 1 else x * frac(x - 1)
printfn "%d" (frac 5)

※ デフォルトで再帰可能になっていないのは、同名の変数で覆い隠すシャドウイングを考慮した言語設計のようです。F#の元になったOCamlについての記事を紹介します。

前方参照

一般論として用語を解説します。

パーサは上から下にコードを読み進めます。進行方向に沿って下が「前方」と表現されます。

     後方
1 aaaa↓
2 bbbb↓
3 cccc↓
     前方

前方で定義されている関数にアクセス(参照)することを「前方参照」と呼びます。下の例ではtest()が前方参照されています。

C# JavaScript

class Test
{
  static void Main()
  {
    test();  // 前方参照
  }
  static void test()
  {
    Console.WriteLine("abc");
  }
}

test();  // 前方参照
function test() {
console.log("abc");
}

※ 直感的には「上が前」のように感じられるので注意が必要です。C言語の前方宣言は呼び出し元から見て「前方にある宣言」ではなく、「前方参照を可能にするための宣言」という意味だと解釈できます。

F#は前方参照ができません。他の言語でもラムダ式だけで記述すると似たような状況になりますが、それと同じだと考えてください。

C# JavaScript F#

test();  // エラー
Action test = () =>
  Console.WriteLine("abc");

test();  // エラー
let test = () =>
  console.log("abc");

test()  // エラー
let test() = printfn "abc"

F#に前方宣言はありません。必ず後方(上)で定義する必要があります。

C# JavaScript F#

Action test = () =>
  Console.WriteLine("abc");
test();  // OK

let test = () =>
  console.log("abc");
test()  // OK

let test() = printfn "abc"
test()  // OK

これは強い制限のようにも感じられますが、コードを読んだり部分的に引用したりするときは、そこより上だけを見ておけば良いという利点があります。

相互再帰

前方宣言はありませんが、相互に再帰する場合は特別な構文があります。

F#ではrecandを使います。C#ではラムダ式を使わなければ特に問題はなく、JavaScriptでは動的に参照されるためアロー関数式でも特に意識する必要はありません。

C# JavaScript F#

void test1() { test2(); }
void test2() { test1(); }

let test1 = () => test2();
let test2 = () => test1();

let rec test1() = test2()
and test2() = test1()

どうしても相互再帰が避けられないケースはありますが、その場合はクラスタとしてひとまとめに定義することが必要です。離して定義することはできません。

関数内関数

C#ではクラス直下のメソッドと、メソッド内のラムダ式の書式が大きく異なります。

※ C# 7ではローカル関数という機能が追加され、この制限が緩和されます。

F#やJavaScriptでは関数の中でも関数が定義できます。

C# JavaScript F#

class Test
{
  static int inc1(int x)
  {
    return x + 1;
  }
  static void test()
  {
    Func<int, int> inc2 = x => x + 1;
    Console.WriteLine(inc1(1));
    Console.WriteLine(inc2(1));
  }
  static void Main()
  {
    test();
  }
}

function inc1(x) {
  return x + 1;
}
function test() {
  function inc2(x) {
    return x + 1;
  }
  console.log(inc1(1));
  console.log(inc2(1));
}
test();

let inc1 x = x + 1
let test() =
  let inc2 x = x + 1
  printfn "%d" (inc1 1)
  printfn "%d" (inc2 1)
test()

カリー化

関数型で必ず話題になるカリー化を説明します。

※ とりあえずF#を使うだけなら必須というわけではありません。分かりにくければ飛ばしても構いません。

必要に応じて次の記事を参照すると良いでしょう。

糖衣構文

以下の3種類はすべて同じ意味です。

  1. let add = fun x -> fun y -> x + y
  2. let add = fun x y -> x + y
  3. let add x y = x + y

2と3は1の糖衣構文です。1をC#/JavaScriptに翻訳して呼び出してみます。

C# JavaScript F#

Func<int, Func<int, int>> add =
    x => y => x + y;
Console.WriteLine(add(1)(2));

let add = x => y => x + y;
console.log(add(1)(2));

let add = fun x -> fun y -> x + y
printfn "%d" (add 1 2)

ラムダ式がネストしています。初見では分かりにくいですが、括弧を付けてみます。

C# JavaScript F#

Func<int, Func<int, int>> add =
    x => (y => x + y);
Console.WriteLine(add(1)(2));

let add = x => (y => x + y);
console.log(add(1)(2));

let add = fun x -> (fun y -> x + y)
printfn "%d" (add 1 2)

JavaScriptではfunctionで記述した方が分かりやすいかもしれません。

JavaScript
let add = function(x) {
  return function(y) {
    return x + y;
  };
};
console.log(add(1)(2));

部分適用

C#/JavaScriptでは引数を1つずつ渡していますが(add(1)(2))、引数を片方だけ渡すこともできます。こうして得られた中間的な関数に残りの引数を渡すと最終的な結果が得られます。

C# JavaScript F#

var inc = add(1);
Console.WriteLine(inc(2));

let inc = add(1);
console.log(inc(2));

let inc = add 1
printfn "%d" (inc 2)

このように引数を途中まで渡して関数を得ることを部分適用と呼びます。部分適用できるように関数の中に関数を入れる形式をカリー化と呼びます。

※ 部分適用が誤ってカリー化と呼ばれることがあるので注意が必要です。

F#では冒頭で挙げた1~3のすべてがカリー化された関数で部分適用できます。カリー化されない関数を定義するには、引数をコンマで区切りタプルとします。引数をタプルで取る関数でもラムダ式でラップすれば擬似的に部分適用は可能です。

F# (カリー化) F# (非カリー化) C# (非カリー化)

let add x y = x + y
let inc = add 1

let add(x, y) = x + y
let inc = fun y -> add(1, y)

Func<int, int, int> add = (x, y) => x + y;
Func<int, int> inc = y => add(1, y);

最初の方で.NETのメソッドの呼び方がネイティブ関数とは異なると述べましたが、引数がタプルとして扱われカリー化されていないためです。

配列

C#ではサイズを指定して配列を作るとゼロで初期化されます。JavaScriptではTypedArrayで同様の処理が可能です。F#では専用の関数を使用します。

C# JavaScript F#

var a = new int[5];

let a = new Int32Array(5);

let a = Array.zeroCreate<int> 5

初期値を指定して配列を作成する方法を示します。F#では要素の区切りはセミコロンなのに注意が必要です(コンマ区切りはタプルを意味するため)。また、F#では配列アクセスで添字の前にドットが必要です。

C# JavaScript F#

var a = new[] {1, 2, 3, 4};
Console.WriteLine(a[2]);

var a = [1, 2, 3, 4];
console.log(a[2]);

let a = [|1; 2; 3; 4|]
printfn "%d" a.[2]

F#では配列をスライスできます。末尾の指定方法がJavaScriptとF#では異なるのに注意します。C#では言語サポートがないため地道にコピーします。

C# JavaScript F#

var b = new int[2];
Array.Copy(a, 2, b, 0, 2);

let b = a.slice(2, 4);
※ 4は末尾の添字+1

let b = a.[2..3]
※ 3は末尾の添字

JavaScriptとF#は文字列の切り出にもスライスが使用可能です。

C# JavaScript F#

"abcde".Substring(2, 2);

"abcde".slice(2, 4);

"abcde".[2..3]

ループ

F#にはwhileforはありますが、continuebreakはありません。再帰で書き直す方法を覚えておくと潰しが効きます。考え方としてはループ変数を引数に見立てて、条件を満たせば再帰的に自分を呼び出します。

C# JavaScript F#

var r = new Random();
for (int i = 0; i < 10; i++)
{
  var v = r.Next(10);
  Console.WriteLine(v);
  if (v > 5) break;
}

for (let i = 0; i < 10; i++) {
  let v = (Math.random() * 10) | 0;
  console.log(v);
  if (v > 5) break;
}

let r = new Random()
let rec loop i =
  if i < 10 then
    let v = r.Next(10)
    printfn "%d" v
    if not(v > 5) then
      loop (i + 1)
loop 0

※ 再帰呼び出しはcontinueに相当して、明示的にcontinueを書かないとループから抜けてしまうと解釈できます。ただしcontinueと違って後続の処理が打ち切られるわけではないため、再帰呼び出しの後に処理が来ないように注意する必要があります。後に処理が来ない再帰を末尾再帰と呼びます。

再帰を使わずにwhileで無理やり実装することもできます。うまく書けないときはこの手で逃げることがあるかもしれません。

C# JavaScript F#

var r = new Random();
int i = 0, v = 0;
while (i < 10 && !(v > 5))
{
  v = r.Next(10);
  Console.WriteLine(v);
  i++;
}

let i = 0, v = 0;
while (i < 10 && !(v > 5)) {
  v = (Math.random() * 10) | 0;
  console.log(v);
  i++;
}

let r = new Random()
let mutable i, v = 0, 0
while i < 10 && not(v > 5) do
  v <- r.Next(10)
  printfn "%d" v
  i <- i + 1

※ この記事では取り上げませんが、F#にはループの代用となる様々な関数が用意されており、本来そちらを使うことが推奨されます。しかしそういったものがうまく適用できないときは、最終手段としてここで説明したような方法でどうにかすることもあるでしょう。

複雑な例

標準入力から文字列を読み取り、先頭から連続する数字だけを抜き出して表示する例を示します。C#では代入した値をそのまま評価できますが、F#ではできないため工夫が必要です。JavaScriptはNode.jsで示します。

C#
string line;
while ((line = Console.ReadLine()) != null)
{
  int i;
  for (i = 0; i < line.Length; i++)
    if (!Char.IsNumber(line[i])) break;
  Console.WriteLine(line.Substring(0, i));
}
JavaScript(Node.js)
let isDigit = ch => "0" <= ch && ch <= "9";

let loop = function*() {
  let line;
  while ((line = yield) != null) {
    let i;
    for (i = 0; i < line.length; i++)
      if (!isDigit(line[i])) break;
    console.log(line.slice(0, i));
  }
}();
loop.next();

let rl = require("readline").createInterface(
  process.stdin, process.stdout, null);
rl.on("line", line => loop.next(line));
rl.on("close", () => loop.next(null));
F#
let rec loop() =
  let line = Console.ReadLine()
  if line <> null then
    let rec loop2 i =
      if i < line.Length && Char.IsNumber(line.[i]) then
        loop2 (i + 1)
      else
        line.[0 .. i - 1]
    printfn "%s" (loop2 0)
    loop()
loop()

※ Windowsで標準入力読み切り型プログラムを終了させるには [Ctrl]+[Z] [Enter] と操作しますが、Node.jsでは [Ctrl]+[D] です。

Node.jsでの標準入力の扱いは次の記事を参考にしました。

参照

F#ではmutableの親戚のような参照という型があります。参照はrefというキーワードを指定するとその場でインスタンスが作られます。

※ C#で引数を参照で渡すためのrefとは別物です。

値へのアクセスはプロパティによる方法と演算子による方法があります。演算子の方がよく使われます。! は参照剥がし(デリファレンス)演算子で、C#の否定演算子とは無関係です。参照への代入は := です。

C# F# (mutable) F# (参照・プロパティ) F# (参照・演算子)

int a = 1;
Console.WriteLine(a);
a = 2;
Console.WriteLine(a);

let mutable a = 1
printfn "%d" a
a <- 2
printfn "%d" a

let b = ref 1
printfn "%d" b.Value
b.Value <- 2
printfn "%d" b.Value

let b = ref 1
printfn "%d" !b
b := 2
printfn "%d" !b

C#よりもC++で説明した方が分かりやすいかもしれません。比較の都合上、C++は参照ではなくポインタで示します。C#でもポインタは使えますが、1要素の配列で表現する方が簡単なのでその方法で示します。

C++ C# JavaScript F#

int *b = new int(1);
printf("%d\n", *b);
*b = 2;
printf("%d\n", *b);

int[] b = new[] {1};
Console.WriteLine(b[0]);
b[0] = 2;
Console.WriteLine(b[0]);

let b = [1];
console.log(b[0]);
b[0] = 2;
console.log(b[0]);

let b = ref 1
printfn "%d" !b
b := 2
printfn "%d" !b

※ C++でも *b = 2;b[0] = 2; に書き換えられます。

クロージャ

関数内関数から外のローカル変数にアクセスできます。これをレキシカルスコープと呼んで、変数への参照をキャプチャと表現します。キャプチャを伴った関数をクロージャと呼びます。

C# JavaScript F#

Action test1 = () => {
  var i = 0;
  Action test2 = () =>
    Console.WriteLine(i);
  test2();
};

let test1 = () => {
  let i = 0;
  let test2 = () =>
    console.log(i);
  test2();
};

let test1() =
  let i = 0
  let test2() =
    printfn "%d" i
  test2()

上の例ではC#やJavaScriptでは値が変更可能な変数をキャプチャしていますが、F#ではmutableな変数はキャプチャできません。Javaでもラムダ式(匿名クラス)からfinalを指定した変数しか参照できないのと似ています。

※ Java 8では初期化以外で値を触らなければ事実上のfinalとしてコンパイルが通ります。下のコードでは意図的にiの値を変更しています。

F# Java 8

let test1() =
  let mutable i = 0
  let test2() =
    printfn "%d" i  // エラー
  i <- 1
  test2()

void test1() {
  int i = 0;
  Runnable test2 = () ->
    System.out.println(i);  // エラー
  i = 1;
  test2.run();
}

F#では参照で回避します。Javaではfinalを付け、中身を変更可能にするため配列で包む回避策があります。

F# Java 8

let test1() =
  let i = ref 0
  let test2() =
    printfn "%d" !i
  i := 1
  test2()

void test1() {
  final int i[] = {0};
  Runnable test2 = () ->
    System.out.println(i[0]);
  i[0] = 1;
  test2.run();
}

理由

ローカルのmutable変数はスタックに確保されるのに対し、参照の実体はヒープに確保されます。スタックに確保された変数はスコープアウト時に破棄されます。しかしクロージャにキャプチャされた変数の寿命はスコープに縛られないため、参照によりヒープに確保して解放はGCに任せることで、スコープアウト問題を回避しています。

C#ではキャプチャされる変数はコンパイラが自動的に扱い方を変えるためこの制限がありません。F#では敢えてエラーにしていると思われます。

クラス

F#とC#で構文がかなり違いますが、protectedや入れ子にされた型がない以外はほぼ同じことが表現できます。JavaScriptは色々な書き方ができますが、ここではF#との対比の都合上classを使わない古い書き方で示します。

C# JavaScript F#

class Num
{
  private int num = 0;
  public void Next()
  {
    Console.WriteLine(++num);
  }
}
class Test
{
  static void Main()
  {
    var n = new Num();
    for (int i = 0; i < 5; i++)
      n.Next();
  }
}

function Num() {
  this.num = 0;
  this.Next = function() {
    console.log(++this.num);
  };
}
let n = new Num();
for (let i = 0; i < 5; i++)
  n.Next();

type Num() =
  let mutable num = 0
  member this.Next() =
    num <- num + 1
    printfn "%d" num
let n = Num()
for i = 0 to 4 do
  n.Next()

※ JavaScriptはfunctionとアロー記法ではthisの扱いが異なります。メソッド内でthisを使う場合はfunctionを使用します。

F#でアクセス制御を省略したときのデフォルトは、letprivatememberpublicとなります。type名の後の()はコンストラクタの引数を表しています。JavaScriptは関数の引数がそのままコンストラクタの引数として使われていて、F#と書き方が似ていることに注目してください。

F#ではインスタンス生成にnewは不要です。IDisposableを実装したクラスではnewを付けないと警告されますが、それ以外ではnewを付けると警告されます。

IDisposableかどうか毎回調べると面倒なので、個人的にはデフォルトで省略して、警告されたら付けるという運用をしています。

コンストラクタ

コンストラクタで引数を処理する例を示します。JavaScriptで引数をそのままキャプチャしているのに注目してください(thisを使用しないためアロー記法)。

C# JavaScript F#

class Num
{
  private int num = 0;
  public Num(int n)
  {
    num = n;
  }
  public void Next()
  {
    Console.WriteLine(++num);
  }
}
class Test
{
  static void Main()
  {
    var n = new Num(5);
    for (int i = 0; i < 5; i++)
      n.Next();
  }
}

function Num(num) {
  this.Next = () =>
    console.log(++num);
}
let n = new Num(5);
for (let i = 0; i < 5; i++)
  n.Next();

type Num(n) =
  let mutable num = n
  member this.Next() =
    num <- num + 1
    printfn "%d" num
let n = Num 5
for i = 0 to 4 do
    n.Next()

クロージャと比較

クロージャをクラスの代わりに使って比較してみます。F#は一旦参照で受けるため冗長になっていますが、C#やJavaScriptはその必要がないため単純です。JavaScriptとF#はクラスの書き方とよく似ているのに注目してください。

C# JavaScript F#

Func<int, Action> Num = num =>
  () => Console.WriteLine(++num);
var next = Num(5);
for (int i = 0; i < 5; i++)
  next();

function Num(num) {
  return () =>
    console.log(++num);
}
let next = Num(5);
for (let i = 0; i < 5; i++)
  next();

let Num(n) =
  let num = ref n
  fun () ->
    num := !num + 1
    printfn "%d" !num
let next = Num 5
for i = 0 to 4 do
  next()

多態

サブクラスによるオーバーライド

数字と足し算のAST(抽象構文木)の例を示します。JavaScriptは継承ではなくダックタイピングで実装します。

C#
abstract class Val
{
  public abstract int Value { get; }
}
class Num : Val
{
  private int num;
  public Num(int n) { num = n; }
  public override int Value { get { return num; } }
}
class Add : Val
{
  private Val a, b;
  public Add(Val a, Val b) {
    this.a = a;
    this.b = b;
  }
  public override int Value
  {
    get { return a.Value + b.Value; }
  }
}
class Test
{
  static void Main() {
    var expr = new Add(new Num(2), new Num(3));
    Console.WriteLine(expr.Value);
  }
}
JavaScript
function Num(num) {
  Object.defineProperty(this, "Value", {
    get: () => num
  });
}
function Add(a, b) {
  Object.defineProperty(this, "Value", {
    get: () => a.Value + b.Value
  });
}
let expr = new Add(new Num(2), new Num(3));
console.log(expr.Value);
F#
[<AbstractClass>]
type Val() =
  abstract Value: int
type Num(num) =
  inherit Val()
  override this.Value = num
type Add(a: Val, b: Val) =
  inherit Val()
  override this.Value = a.Value + b.Value
let expr = Add(Num 2, Num 3)
printfn "%d" expr.Value

判別共用体

F#には多態を簡潔に表現する判別共用体というものがあります。クラスごとの評価関数を個別にオーバーライドするのではなく、一箇所にまとめて書くスタイルです。

F#
type Val =
| Num of int
| Add of Val * Val

let rec eval = function
| Num(num) -> num
| Add(a, b) -> eval a + eval b

let expr = Add(Num 2, Num 3)
printfn "%d" (eval expr)

判別共用体がうまくハマるパターンではものすごく簡潔になります。

JavaScriptで雰囲気を真似た例を示します。C#では冗長になるため省略します。

JavaScript
function Num(num) {
  this.num = num;
}
function Add(a, b) {
  this.a = a;
  this.b = b;
}
function eval(v) {
  if (v instanceof Num)
    return v.num;
  if (v instanceof Add)
    return eval(v.a) + eval(v.b);
}
let expr = new Add(new Num(2), new Num(3));
console.log(eval(expr));

資料

今回と同じような視点の記事を紹介します。

この記事の元になった記事です。C#的なオブジェクト指向構文を中心にまとめています。

限定された範囲内でC#をF#に変換するトランスレータです。JavaScriptで実装されているため、ブラウザ上で動きます。

C#とF#との対比記事です。引用されているF# Tutorialはプロジェクト作成のときに選択できます。

63
59
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
63
59

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?