仕事で使うf#
TRANSCRIPT
タプル
.
タプルの例1
.
.
.
let x, y = 10, 20
printfn "%A" (x, y) // => (10, 20)
.
タプルの例2
.
.
.
let (x, y) as tpl = 1, 2
printfn "%A" tpl // => (1, 2)
printfn "%A" (x, y) // => (1, 2)
printfn "(%d, %d)" x y // => (1, 2)
リストじゃ駄目なの?
.
リストでやってみる
.
.
.
let [x; y] as lst = [1; 2]
printfn "%A" lst // => [1; 2]
printfn "%A" [x; y] // => [1; 2]
printfn "[%d; %d]" x y // => [1; 2]
.
コンパイル結果
.
.
.
warning FS0025: この式のパターン一致が不完全ですたとえば、値 ’[_;_;_]’ はパターンに含まれないケースを示す可能性があります。
タプルとリストの違い1
型の違いは容易にコードに落とし込めるタプルは保持する要素の違いが型の違いとなるなので、タプルは型安全に要素をばらせる
変数の型は動的に変更できないリストは要素数の違いは型に影響しないなので、リストは要素の追加、削除が可能
タプルとリストの違い2
.
タプル
.
.
.
(2, "hoge") // int * string
.
リスト
.
.
.
// コンパイルエラー[2; "hoge"]
.
.
[box 1; box "hoge"] // obj list
レコードとタプル
.
タプルの場合
.
.
.
// 名前, 年齢let p = "山田太郎", 21
.
レコードの場合
.
.
.
type Person = {
Name: string
Age: int
}
let p = { Name = "山田太郎"; Age = 21 }
レコードにすると、コードは長くなるけどコメント不要に!感覚的には 4要素のタプルは辛い
レコードとクラス
F#にはレコードのほかにクラスもある
.
クラス
.
.
.
type Person(name: string, age: int) =
member this.Name = name
member this.Age = age
let p = Person("山田太郎", 21)
.
レコード
.
.
.
type Person = { Name: string; Age: int }
let p = { Name = "山田太郎"; Age = 21 }
どう使い分ければいいの?
レコードとクラス
.
クラス
.
.
.
type Person(name: string, age: int) =
member x.Name = name
member x.Age = age
let f p = p.Name
.
コンパイル結果
.
.
.
error FS0072: このプログラムの場所の前方にある情報に基づく不確定の型のオブジェクトに対する参照です。場合によっては、オブジェクトの型を制約する型の注釈がこのプログラムの場所の前に必要です。この操作で参照が解決される可能性があります。
レコードとクラス
.
レコード
.
.
.
type Person = { Name: string; Age: int }
let f p = p.Name
.
コンパイル結果
.
.
.
type Person =
{Name: string;
Age: int;}
val f : Person -> string
推論できた!
C#でやるとしたら
.
クラス階層で
.
.
.
public abstract class HtmlElem {}
public class Heading : HtmlElem {
public int Level { get; private set; }
public string Text { get; private set; }
public Heading(int level, string txt) {
Level = level; Text = txt; } }
public class Text : HtmlElem {
public string Text { get; private set; }
public Text(string txt) {
Text = txt; } }
public class HorizontalLine : HtmlElem {}
F#でやるとしたら
.
判別共用体で
.
.
.
type HtmlElem =
| Text of string
| Heading of int * string
| HorizontalLine
クラス階層の欠点
参照値の比較しかできない内容の比較を行うためには、Equalsメソッドをオーバーライドする必要があるGetHashCodeも実装する必要があるMake static analyzers happy==演算子と!=演算子もオーバーロードしておくと便利
文字列化できないテスト失敗時のメッセージがへぼいToStringメソッドをオーバーライドする必要がある
なのに長い
パターンマッチ
リストやタプル、レコード、判別共用体と言った構造的な型を分解する
ネストした構造から一発で望みの値を手に入れることができる
不足しているケースを検出してくれる
絶対にマッチしないケースを検出してくれる
判別共用体に対するマッチ
.
HtmlElemをHTML文字列に変換する
.
.
.
type HtmlElem =
| Heading of int * string
| Text of string
| HorizontalLine
let toHtmlStr elem =
let h lv s =
sprintf "<h%d>%s</h%d>" lv s lv
match elem with
| Heading(level, txt) -> h level txt
| Text txt -> "<p>" + txt + "</p>"
| HorizontalLine -> "<hr/>"
そのものずばりの、match式を使いました
タプルに対するマッチ
.
渡された整数を足す
.
.
.
let add = function
| 0, y -> y
| x, 0 -> x
| x, y -> x + y
最後の引数に対してマッチさせる、function式を使いました
match式と function式
.
match式
.
.
.
let add tpl =
match tpl with
| x, y -> x + y
.
function式
.
.
.
let add = function
| x, y -> x + y
レコードに対するマッチ
.
特定のフィールドを取り出す
.
.
.
type t = { Tag: int; Value: int }
let value { Value = v } = v
引数に対してマッチさせました
match式と引数に対するマッチ
.
match式
.
.
.
let add tpl =
match tpl with
| x, y -> x + y
.
function式
.
.
.
let add = function
| x, y -> x + y
.
引数に対するマッチ
.
.
.
let add (x, y) = x + y
ネストしていても大丈夫
.
Personから名前を取り出す
.
.
.
type Name = {
FirstName: string
LastName: string
}
type Person = { Name: Name; Age: int }
let f x =
let { Name = { FirstName = fn } } = x
fn
例えば
.
Maybeモナド
.
.
.
type MaybeBuilder() =
member this.Bind(x, f) =
x |> Option.bind f
member this.Return(x) = Some x
let maybe = MaybeBuilder()
let plus db = maybe {
let! x = db |> Map.tryFind "x"
let! y = db |> Map.tryFind "y"
return x + y
}
F#の場合はどうよ?
.
foldと reduceの型
.
.
.
> List.fold;;
val it : ((’a -> ’b -> ’a) -> ’a -> ’b list -> ’a)
= <fun:clo@1>
> List.reduce;;
val it : ((’a -> ’a -> ’a) -> ’a list -> ’a)
= <fun:clo@2-1>
引数の数と、型引数の数が違うfoldは初期値を受け取り、結果の型は初期値と同じ型reduceは初期値として listの先頭要素を使う。結果の型は必然的に listの要素の型
foldで色々と書く
では foldを使って色々と書いてみましょう!
sum・・・全部足す
forall・・・全ての要素が条件を満たすかどうか
map・・・一つ一つの要素を変換する
filter・・・条件を満たす要素のみを集める
foldで forall
.
forall
.
.
.
let forall p xs =
xs
|> List.fold (fun acc x -> acc && (p x)) true
今までとは違い、結果の型がリストの要素の型と一致しない例existsとか書いてみるといいかも
foldでmap
.
map
.
.
.
let map f xs =
([], xs)
||> List.fold (fun acc x ->
acc @ [f x]
)
初期値を空リストにするのがミソ
foldでfilter
.
filter
.
.
.
let filter p xs =
([], xs)
||> List.fold begin acc x ->
if p x then acc @ [x]
else acc
end
ラムダ式の中で分岐
.NET Frameworkのバージョン
F#は.NET Framework2.0が入っていればRuntimeを入れるだけで動く
.NET Framework2.0では LINQが使えないけど、F#なら Listなどに対する高階関数が色々と用意されている
非同期ワークフローも使える
.NET Frameworkのバージョンが 2.0だと、F#を採用することのメリットが絶大!
.NET Frameworkのバージョン
ちなみにバージョンが 4.0だとしても F#は素敵。例えば、TDDBC Tokyo 1.6の課題を見ると・・・
C#の場合 (実装コード)KeyValueTime.cs 42行SystemClock.cs 30行TddbcKeyValueStore.cs 67行
F#の場合 (実装コード)KeyValueStore.fs 48行
C#が 139行に対してF#は 48行!
.NET Frameworkのバージョン
ちなみにバージョンが 4.0だとしても F#は素敵。例えば、TDDBC Tokyo 1.6の課題を見ると・・・
C#の場合 (テストコード)KeyValueTimeTest.cs 141行SystemClockTest.cs 34行TddbcKeyValueStoreTest.cs 346行 (!)
F#の場合 (テストコード)KeyValueStore.fs 170行
C#が 521行に対してF#は 170行!
F#の特長の売り込み
他の.NET言語ではなく、F#を選択する理由より安全
optionとか
よりシンプルさっき見たとおり
より便利各種高階関数型を作る心理的ハードルが低い→型が軽いパターンマッチと網羅性のチェック
大体こんな感じ
判別共用体
判別共用体を外に見せてしまうと・・・
継承階層に置き換わる各ケース識別子
値を持たないケースは staticフィールド、値を持つケースは入れ子になったクラスのNewHogeメソッド
どのケース識別子かを整数で表すTagプロパティと、あるケースかどうかを返す IsHogeメソッド
こんなの使ってられっか!状態
判別共用体は F#外に見せないようにしましょう
関数
普通の関数なら変換してくれるんだけど・・・
.
.
module Hoge
let plus10 = ((+)10)
これだとHoge.plus10プロパティとして見える・・・しかも FSharp.Core.FSharpFuncクラス→カリー化されている上にFSharp.Core.dllを参照に追加する必要
変数に関数を入れているときは注意!
パーサジェネレータは?
yaccとか lexとかでは駄目なの?
yaccや lexはホスト言語とは別言語
コンパイル一発で動かない
汎用言語ではないので、柔軟性が低い
部品の共通化とか悲しい
テストがつらい
そこでパーサコンビネータですよ!
再帰的なパーサの作りやすさ
NParsecでは再帰的なパーサを作るために配列を使っていて最初見たときにわけがわからない。
.
こんな感じ
.
.
.
var lazyexpr = new Parser<double>[1];
var plazyexpr = Parsers.Lazy<double>(() =>
lazyexpr[0]
);
var pterm =
plazyexpr.Between(popen, pclose) | pnum;
var pexpr = ...
lazyexpr[0] = pexpr;
再帰的なパーサの作りやすさ
FParsecの場合は
.
こんな感じ
.
.
.
let pexpr, exprref =
createParserForwardedToRef()
let pterm =
pexpr |> between popen pclose
<|> pnum
do exprref := ...
マジックナンバー不要!
データ構造の作りやすさ
パーサコンビネータと判別共用体の相性は抜群。F#だと 10行のところが、C#では 54行・・・
さらにC#ではパターンマッチが無い→いろいろやろうと思ったらVisitorパターン実装・・・となって更に膨らむ
コンピュテーション式が使える
NParsecは直接Bindメソッド呼び出したり、return Parsers.Return(...)とか、カオスFParsecはコンピュテーション式が使えるのでシンプルに記述できる!
これは後で見てみます