Tambourine作業メモ

主にスキル習得のためにやった作業のメモ。他人には基本的に無用のものです。

F#で遊んでみる(7)

6章を読んでいこう。6章は純粋な関数型言語からはみ出す話である。

まず、モジュールの説明で、例のためにじゃんけんモジュールが例示されている。

type Jyanken = Gu | Ti | Pa

[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module Jyanken =
    type Result = P1Win | TieGame | P2Win
    let random = System.Random()

    let cpu () = (略)

    let play p1 p2 = (略)

    let vsCPU you =
        let cpu = cpu()
        let res = match play you cpu with
                  | P1Win -> "Win!"
                  | TieGame -> "Aiko"
                  | P2Win -> "Lose..."
        sprintf "[%s] You:%A VS Cpu:%A" res you cpu

これを読み込んだ上で、Jyanken.vsCpu Gu;;のようにして遊ぶ。

面白いのは、ここでJyankenという型とモジュールを両方定義してしまうこと。 これはF#的には自然なことだそうな。 というか、OOP的な考え方で言えば、型とそれに紐付く関数(メソッドといってもいいかもしれない)を セットで扱っているんだから、確かに自然だ。

ところが、.NET世界的に言えば、同じ名前空間に型とモジュールが両方いるのはマズい。というわけで、

[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]

という属性を付けておく。これを付けると、.NET世界から見ると、JyankenモジュールはJyankenModuleという後ろにModuleというサフィックスが付いた名前に見えるんだそうな。なるほどね。

続きを見ていこう

  • アクセス制御
    • デフォルトはpublic。モジュール内に限りたい場合は、private。アセンブリ内に限りたい(これは.NETの概念ですな)場合は、internalを付ける。
  • ループ
    • while...do
    • for...to (OCamlとの互換性のためにある。for...inがあればfor...toはいらない)
    • for...in

次に参照セルの話になる。ミュータブルなオブジェクトをF#で扱うための機構だね。.NET世界と上手くやっていくにはどうしても必要になるんだろう。

:=とか!とか今まで出てこなかった演算子使って、こういう感じで使うよ・・・という例をやってみると、このやり方は非推奨になっているようだ

> let m = ref 42;;
val m: int ref = { contents = 42 }

> m := 24;;

  m := 24;;
  --^^

/Users/tambara/study/fs_study/stdin(2,3): info FS3370: F# ライブラリからの ':=' の使用は非推奨です。https://aka.ms/fsharp-refcell-ops を参照してください。たとえば、'cell : = expr' を 'cell.Value <- expr' に変えてください。

val it: unit = ()

> m;;
val it: int ref = { contents = 24 }

> !m;;

  !m;;
  ^

/Users/tambara/study/fs_study/stdin(4,1): info FS3370: F# ライブラリからの '!' の使用は非推奨です。「https://aka.ms/fsharp-refcell-ops」を参照してください。たとえば、'!cell' を 'cell.Value' に変えてください。

val it: int = 24

上で示されているリンクはこれである。去年の10月に出た提案みたい。

github.com

F#6の話みたいなので、F#6の新機能を見てみた。ここに出てるんだね。

F# 6 の新機能 - F# ガイド | Microsoft Docs

最新の F# コーディングでは、通常は代わりに let mutable を使用できるため、参照セルはほとんど必要ありません。

ということなので、まあ、参照セルは使わない方がいいのかな。

次はクロージャ。なぜクロージャが参照セルの後に来ているかというと、クロージャで変数をキャプチャするケースは、往々にしてミュータブルなものを扱いたいから・・・だと思う。

本では、ミュータブルな変数をキャプチャすることは出来ないので参照セルを使わなければならないとしている。けど、そのエラー例を入力するとF#6では普通に定義できちゃう。これも変わったんだね。

> let counter =
-     let mutable n = 0
-     (fun () -> n <- n + 1
-                n)
- ;;
val counter: (unit -> int)

次は、参照渡し。.NETには参照渡しの機能がある。VB.netのByRefっぽい。

> let swap (x: byref<'T>) (y: byref<'T>) =
-     let z = x
-     x <- y
-     y <- z
- ;;
val swap: x: byref<'T> -> y: byref<'T> -> unit

> let mutable x, y = 'X','Y';;
val mutable y: char = 'Y'
val mutable x: char = 'X'

> swap &x &y;;
val it: unit = ()

> x, y;;
val it: char * char = ('Y', 'X')

関数定義にbyrefと書いてしまうと、それはもうその型ではないから、呼び出すときに参照にしなきゃいけない。ちょっとRustっぽい。

一方、C#の参照渡しには、ref, in, outなど使い方を制約するようなキーワードがあります。 普通のrefはすでに存在する変数を変更して欲しくて渡すわけですが、outは「ここに入れて値を返してね」と未初期化の変数を渡すモノです。F#的には、複数の値を戻したければタプルで返すなり、DUを定義して返すなりすればいいのでこういうものはいらないんだけど、すでにそう定義されているメソッドを使わなきゃいけないことはある。例えば、Int32.TryParseC#でこう定義されている。

public static bool TryParse (string? s, out int result);

Int32.TryParse メソッド (System) | Microsoft Docs

これをどうやって呼び出せばいいか。だって初期化してない変数なんてF#では定義できるのか?

> let mutable n = Unchecked.defaultof<int>;;
val mutable n: int = 0

> System.Int32.TryParse("128", &n);;
val it: bool = true

> n;;
val it: int = 128

すごい無理矢理感がある(笑)

どうしてこんな無理矢理感な文法にしているかというと、なんのことはない、F#はout引数をタプルで送り返すという機能を持っているからなのだった。まあ、無理にやりたいんならどうぞって感じ?

> System.Int32.TryParse "128";;
val it: bool * int = (true, 128)

ただし、これはメソッドだけの機能で、関数ではこれは出来ないらしい。型システム的にまずいことが起きうるんだそうな。

次は、遅延評価。遅延評価をしたければ、lazyする。するとLazy<'T>という型になる。

> let mutable n1, n2 = 0, 0;;
val mutable n2: int = 0
val mutable n1: int = 0


> let counter1 = fun () -> n1 <- n1 + 1
-                          n1
- ;;
val counter1: unit -> int

> let counter2 = lazy (n2 <- n2 + 1 
-                      n2)
- ;;
val counter2: Lazy<int> = Value is not created.

> let i1 = counter1 () + counter1 () + counter1 ();;
val i1: int = 6  // 1 + 2 + 3

> let i2 = counter2 + counter2 + counter2;;

  let i2 = counter2 + counter2 + counter2;;
  --------------------^^^^^^^^

/Users/tambara/study/fs_study/stdin(13,21): error FS0001: 型 'Lazy<int>' は演算子 '+' をサポートしていません

> let i2 = counter2.Force() + counter2.Force() + counter2.Force();;
val i2: int = 3 // 何度Forceしても1のまま

そして、標準入出力。普通にstdin, stdout, stderrというオブジェクトを使えばいい。 例えば、stdinを呼ぶと、TextReader型のSystem.Console.Inプロパティの値が取れる。これは完全に.NETの世界なので、stdin.ReadLine()とかすればいい。でも、戻りはstring?なんだけど、これはoptionが返ってくるのかな?よくわからない。

この章の最後は、use束縛。C#のusingである。letじゃなくてuseで束縛しておけば、スコープから外れたときにリソースの解放を自動でしてくれる(IDisposableなら)。まあ、それだけ。usingもある。usingは2つの引数を釣る関数で、1つめの引数にIDisposableを、2つ目にそれを使った関数を渡す。