普段、C# を触ってるのですが、switch
文で苦しい思いをすることがたびたびあります。C# 7.0 リリースはよ、と思いつつもいい書き方ないかなぁと探すといい記事に巡りあうことができます。
単純に switch-case の代替としてのパターンマッチングも十分好きなのですが*1 、複数の値の組み合わせやタプルの名前つけと分解は直感的でもあり好きです。そんなときに、@t_tetsuzin さんから、LINQPad で触れること、Option や Either、Maybe から アクティブパターンなどもっと面白いといわれたのでいい機会なので触ってみようと思います。
私の頭はアクティブパターンのURL 送られてもすぐに理解できるような賢さは持ってないのですよ。
目次
ごにょごにょ触る環境の構築
C# でごにょごにょコード篇を書いては捨て、値試しつつ書き方とか探すときに使っているのが LINQPad です。LINQPad 自体は伝道師の素晴らしい記事があるのでそちらで。
LINQPadで F# 確かにサポートされてました。手元のは LINQPad Premium なのでちょうどよさそうです。
ではまず開発環境を作りましょう。
Visual Studio でのF#
Visual Studio を標準でインストールします。残念ながらカスタムにするのは HoloLens ぐらいのためです。F# ここで入れろ? 自動化してるのでカスタマイズめんどくさいです。却下。
VSのインストール後は、F# Tools を OneGetで入れます。
Install-Package visualfsharptools
これで Visual Studio で F# プロジェクトを触ったりビルドができるようになります。
LINQPad での F#インテリセンス
LinqPad で F# をするには少し前準備が必要です。単純に F# Expression や F# Class にしてもLINQPad エラーが発生します。
インテリセンス実行しようとするところでぐぬぬ
View More Details
から詳細エラーをみると、AnnouncementClient が見つからないとのこと
System.ServiceModel.Discovery 名前空間にあるので、System.ServiceModel.Discovery.dll をインポートします。
AnnouncementClient クラス (System.ServiceModel.Discovery) | Microsoft Learn
それでもエラーが出ます。今度は MetadataExchangeClient です。
System.ServiceModel.Description.dll はありません。System.ServiceModel.dll をインポートします。
これで System.ServiceModel.dll
と System.ServiceModel.Discovery.dll
を参照に加えました。
無事にインテリセンスが出ました。まずは第一歩です。
LINQPad で dump
個人的に、LINQPad を使う良さの90%は .Dump() メソッドにあります。VS のローカル変数と同様に オブジェクトを覗きつつ次にメソッドをつなげることもできるので、ひじょーに強力です。
ただ、F# でメソッド呼び出しは気持ち悪いので、パイプラインから dump を呼べるようにします。
@guitarrapc_tech obj型を引数にとるdumpメソッドを作ってパイプラインで渡しましょう
— 徹人 (@t_tetsuzin) September 3, 2016
F# は smallCamel ベースのようなので、当初、Dumpではなく dump関数をつくってたのです。
let dump x = Dump x:Object
が、まぁ当然 型情報が失われます。
ジェネリクスどう書けばいいのかなぁ、とおもっていたら inline を使うといいよという情報が。
@guitarrapc_tech dump はこの書き方でどうでしょ? pic.twitter.com/t97SEV8U5Y
— 徹人 (@t_tetsuzin) September 3, 2016
うまく取れました。
inline って何かと思ったら、型推論しておいて コンパイル時に型確定してその型の分だけ関数を生成するらしい... すごい..。
@guitarrapc_tech やったー!!₍₍ (ง ˘ω˘ )ว ⁾⁾
— 徹人 (@t_tetsuzin) September 3, 2016
F# はメソッド定義した後にどういう使われ方をしたかで型を明記していない引数の型が確定するのですが、inline つけると使われた数だけメソッドを生成してくれます。templateに近いのかな?
@guitarrapc_tech @t_tetsuzin コンパイル中に推測するんです。だから普通に書くと、ジェネリックメソッドではなく、きちんと型が確定します。で、そうじゃなくてジェネリックメソッドにしたい場合は、inlineを付けます
— にゃーん (@kekyo2) September 3, 2016
@guitarrapc_tech let dump x = Dump x と書くと、Dumpメソッド(?)の引数の型が推測されて dump xの引数はその型になります。
— にゃーん (@kekyo2) September 3, 2016
let dump (x:Object) = Dump xと書くと、明示的にObjectと指定した事に
@guitarrapc_tech インライン関数についての記述見つけました。https://t.co/Qe9FkA3Zwk
— 徹人 (@t_tetsuzin) September 3, 2016
上記にあるように、inline 付けた場合は呼ばれた箇所で記述されたかのような扱いになるので、濫用しすぎるとコンパイル後のサイズが巨大になるらしいです。
inline と rec の併用できないんですねー。ほむほむ。
2016/9/4 修正
型注釈...! なるほどキャストとは違うのですね。型推論が強力というか前提というか、ほむほむ。象徴的に感じます。
@kekyo2 @guitarrapc_tech
— はぇ~☆ (@haxe) September 3, 2016
let dump x = Dump x : Object
のほうですが、「Dump x : Object」の部分だけを見て「Dump x」の戻り値型がObjectであるという注釈として解釈されます。
修正ここまで
さて、もう1つほしいのが、.Dump(.Dump()
に名前を付けて表示できるので。適当に書くとエラーが。
むむっと思っていると、Overload はツラいので別名の関数がよさそうとのこと。パイプラインの入力は優先度が低くて後から入ってくるのは PowerShell の ValueFromRemainingArguments
も似た挙動ですね。
@guitarrapc_tech それと、さっきのジェネリックなDumpの書き方ですが、やはりオーバーロードと型推論の相性が悪いので dumpWithDepth みたいな別名のメソッドをオーバーロードの数だけ作ることに……。F# のツラいところです……。
— 徹人 (@t_tetsuzin) September 3, 2016
about Functions Advanced Parameters - PowerShell | Microsoft Learn
適当に書いてみました。
let inline dumpTitle x y = y.Dump(x:string); y
ここで 関数の部分適用 を学びました。なにこれ便利。メソッドでも似たようものですが、変数と関数が両方 let
でかけることもあり格段に便利です。
@guitarrapc_tech そうなんですよ。パイプラインは優先度が少し低めなんです。
— 徹人 (@t_tetsuzin) September 3, 2016
あと、関数の部分適用使えば予め title を設定した dump を作って使い回せたりします。 pic.twitter.com/AZMU6jlQjP
さて、inline でいいかなぁと思っていたら、なんとinline
使わずに普通にかけそう。
@kekyo2 演算子やメソッドのオーバーロード解決をするときに型が1つに決められてしまうというやつですね。そこに注意すれば、inlineなしにジェネリックな定義ができる場合もありますし、そのようなジェネリックな関数を呼び出すだけの関数なら同様にジェネリックに推論されます
— Kentaro Inomata (@matarillo) September 3, 2016
ということで最終版
これで、パイプラインで値を確認しつつ先につなぐことも簡単です。
let dump x = x.Dump(); x let add x y = x + y let square x = x * x let negete x = x * -1 let triple x = x * x * x let print (x : Object) = Console.WriteLine(x); square 42 |> negete |> triple |> dump |> square |> print
生きていけそう。
let dump x = x.Dump(); x // let inline dump x = x.Dump; x にするとコンパイル時に利用している型の分だけ関数が生成される (なんちゃってジェネリクス代わりにはなりそう let dumpTitle (x : string) y = y.Dump(x); y let titleDump = dumpTitle "れんしゅー" let hello = "Hello" + "World" |> dump hello |> dumpTitle "文字列" hello |> titleDump
チュートリアル
さて、F# といえば、The F# Software Foundation
です。
しかしみてもいい感じの F# Tutorial みたいなサイトはありません。ということで、上からとググって見つけたいくつかを参考に見ていきます。
サイト | 概要 |
---|---|
言語ガイド - F# | Microsoft Learn | 言語仕様です |
F# Programming - Wikibooks, open books for an open world | Wiki ですが、想像以上にきれいにまとまっていたりサンプルが多いのでいい感じです。 |
fsharp-cheatsheet | チートシートといいつつ、結構ほんとここを参考にしました。 |
neue cc - F# TutorialをC#と比較しながらでF#を学ぶ | 漂白されていません。何気に一番ピンとくるぐらい具体的に書かれています。 |
文字列 - F# と C# の記法比較 | 結構知りたい情報あって嬉しいです。 |
タプル - F# と C# の記法比較 | Tuple もここがわかりやすかったです |
F# チュートリアル、むむっと思っていたのですが、記事書きつつ見直していて Try F# がプレイグラウンドもあるし最高な気がします。
気を取り直して順番に見ていきます。といっても、このあたり と軌跡は似てます。
変数 と関数の 宣言
基本中の基本です。open とかはいったん知らないふりで。
let
で変数も関数も宣言できるようです。
let str = "stringだぞ" // System.String 型の変数 str を宣言
C# のvar と同様に型推論効くのですんなりです。末尾セミコロンレス なのですね。代入は不可なのはいいとして、これにさらに =
オペレータを使うと比較になるのは少し驚きでした。C# なら ==
、PowerShell なら-eq
のような比較演算子を別途用意しないのですね。
let str = "stringだぞ" str = "hoge" // これは比較
2016/9/4 修正
大事なことなのに忘れそう.... 理解しました。
@guitarrapc_tech F#には代入は存在しません。letは(たとえ同名であっても)新しい変数を作ります。「let 変数名 = 値 in 式」ここまでで1つの構文です。(inの代わりに改行でも可)
— はぇ~☆ (@haxe) September 3, 2016
ですので演算子としての = は比較しかありません。
修正ここまで
とりあえず 定番ですね。
printfn "Hello World"
文字列埋め込み
print debug するなりなんでも、文字列へ変換できないことには学習もままならないのです。で、文字列どうやって埋め込むかと思ったら printfn
に int埋め込み だと %d
とか書くらしく。
printfn "Hello World %d!!" 1
まさか let すると 直でかけないとは...ほむ。
let hello = "Hello" + " World" printfn "%s" hello
string.Format や Console.WriteLine がかけるということで、もう、こっちのほうが書きやすい..。
Console.WriteLine("Hello World {0}!!", 1)
この時点の気分は完全にこれです。
100% 完璧同感で速攻 |> dump 作ったんだけど、Console.WriteLine x 最強なの....? つらすぎる... 一覧探すのに F# %d ってして見つけたサイトだしな... pic.twitter.com/dnONOlcyIV
— guitarrapc_tech (@guitarrapc_tech) September 3, 2016
@kekyo2 これ無理です。C# でも string interlope でてからは $"natoka : {x}" とかなわけですが、F#でも同様に書けるといいにゃぁあと。
— guitarrapc_tech (@guitarrapc_tech) September 3, 2016
Boxing かかってもこれでいいんじゃないか気分になったところです。
let print (x : Object) = Console.WriteLine(x);
ここで、sprintfn
なんて便利関数があるということでサクッと乗せ換えです。*2
これで、dump
関数につないで生きていけそうです。
sprintf "Hello %s! Number is %d" "world" 42 |> dump sprintf "Hello %s! Number is %d. %s" "world" 42 "hoge" |> dump
そういえば、型指定は string でもいいのにメソッドの時は String なんですね。
2016/9/4 修正
System.String と string というより、なるほど用途... 言われないと気付けなかった気がします。
@guitarrapc_tech
— はぇ~☆ (@haxe) September 3, 2016
.NETの世界のSystem.Stringクラス(標準でopenされていないので名前空間が必要)
型名としてのstring型略称(C#のstringキーワードと違ってクラスではなく型名だけの意味)
続く
@guitarrapc_tech
— はぇ~☆ (@haxe) September 3, 2016
キャスト演算子としてのstring関数(Object.ToString()と同じで何でもstringに変換。型略称とは別の存在)
F#の文字列機能を提供するStringモジュール(1枚目の画像)
の4種類が存在します。
@haxe なるほどー。C# では string や long でも動作の違いがない前提で使ってましたが、F# ではString や Int64 のほうが統一できそう....?でしょうか
— guitarrapc_tech (@guitarrapc_tech) September 4, 2016
@guitarrapc_tech えーとYesでもNoでもないです。C#ですとstringはSystem.Stringのエイリアスであり両者はほぼ等価でどちらを使うか宗教戦争になっていますが、F#はそうではありません。
— はぇ~☆ (@haxe) September 4, 2016
@guitarrapc_tech StringやInt64では統一できません。型名は3番目に紹介したキャスト演算子にはなり得ません。(ややこしいことにコンストラクターにはなるので、コンストラクター引数にある型に関してはキャストのように扱えます。)
— はぇ~☆ (@haxe) September 4, 2016
@guitarrapc_tech 2番目に紹介した型略称は型名としてしか使えず、これに関してはStringやInt64で代用できるので、型略称を使わないのは1つの手です。
— はぇ~☆ (@haxe) September 4, 2016
修正ここまで
正直、文字列にちょちょいとデバッグ見たいという時なら、%A
でいい感じに自動的に変換してくれるのばかり使いそうな気がします。
部分文字列抽出
さて、続けて部分文字列抽出です。C# なら string.Substring()
ですが、 F# はスライシングっぽく書けるようです。便利。
let str1 = "hoge" printfn "%s" (str1.[0..2])
文字列連結は + と ^ の両方でいけるようで。なんで2つのオペレータがあるんですかね。不思議。
2016/9/4 修正
なるほど OCaml 由来。
@guitarrapc_tech C#由来の+とOCaml由来の^ですねぇ~
— はぇ~☆ (@haxe) September 3, 2016
修正ここまで
String の Char 取り出しは、str.Chars(<int>)
か str1.[0]
。ただし .[<int>]
だと Char になって .[<int>..<int>]
だと string なのは型推論強いけど、うぬぬ。
let str1 = "hoge" printfn "%c" (str1.[0]) printfn "%s" (str1.[0..1])
2016/9/4 修正
言われてみると .Chars は C# で使ってことなかったです。
@guitarrapc_tech 理解は正しいのですが、コレクションからの1要素取り出しとスライシングです。文字列だけでなく、配列、リストにも適用できます。.Charsは.NETの機能です。C#では隠ぺいされていてアクセスできなくなっていますが。
— はぇ~☆ (@haxe) September 3, 2016
修正ここまで
List
List 処理はびっくりでした。正直F# すごい。[]
でくくって、;
で要素を分割。::
で1つの文字とListの連結 *3、@
で合成。
簡単です。PowerShell とか目じゃない。
let list1 = [ "a"; "b" ] let list2 = "c" :: list1 let list3 = list1 @ list2
さらに
@guitarrapc_tech 先ほど配列の操作についてほめていたのでこれも投下しちゃいます pic.twitter.com/iPbcaDBXGZ
— 徹人 (@t_tetsuzin) September 3, 2016
yield!
で List内 List の展開までされてあら素敵。seq や PowerShell 同様 ..
でつなげたり、途中の値の指定もできて便利。
なるほど(?)
@guitarrapc_tech yield! がキモですね。配列的なリテラルである list array seq の内包表記で使える書き方です。その場で配列を展開してそれぞれに yield かけるのと同じ事してくれます。再帰と組み合わせてツリー型のデータなんか一発です
— 徹人 (@t_tetsuzin) September 3, 2016
関数とパイプライン
関数も変数同様 let
で宣言できて、引数は 関数名の後ろで指定と。
let f x = x*x f 2 |> dump
型指定はなるほど
@guitarrapc_tech let dump x = Dump x と書くと、Dumpメソッド(?)の引数の型が推測されて dump xの引数はその型になります。
— にゃーん (@kekyo2) September 3, 2016
let dump (x:Object) = Dump xと書くと、明示的にObjectと指定した事に
F# といえば、私の中では パイプライン です。*4印象は素直だにゃぁと。これは凄く直感的で使いやすいと思います。ぐいぐいつなげられます。
let add x y = x + y let square x = x * x let negete x = x * -1 let triple x = x * x * x let print (x : Object) = Console.WriteLine(x); let dump x = x.Dump(); x let dumpTitle (x : string) y = y.Dump(x); y square 42 |> negete |> triple |> dump |> square |> print
ということで、List と合わせて触ってみると同じ結果が色々かけるのですが、どれがいいんですかねぇ。
List.map square [0..10] |> dumpTitle "まっぷ" List.map (fun x -> x*x) [0..10] |> dumpTitle "まっぷ2" [0..10] |> List.map square |> dumpTitle "まっぷ3" [ for x in 0..10 -> square x ] |> dumpTitle "リスト内包表記"
あ、リスト内包表記の時だけ、Dump壊れるのなんとかならないでしょうか.... ツラい。
2016/9/4 修正
式が返ってました。[ for x in 0..10 -> square x ] |> dumpTitle "リスト内包表記"
でok
修正ここまで
再帰関数は rec
キーワードを付けた関数にすると。個人的に再帰の明示は結構好きです。お勉強かねてif使いつつフィボナッチ数列書いてみました。
let dump x = x.Dump(); x let dumpTitle (x : string) y = y.Dump(x); y let rec fibonacc n = if n=0 then 0 elif n=1 then 1 else fibonacc(n-1) + fibonacc(n-2); fibonacc 7 |> dump
なるほど、そのまんまひねりなしでok。
パターンマッチ
先ほどのif..else 連打きもいので導入篇ということで触ってみます。
let dump x = x.Dump(); x let dumpTitle (x : string) y = y.Dump(x); y let rec fib n = match n with | 0 -> 0 | 1 -> 1 | n -> fib(n-1) + fib(n-2) fib 7 |> dump
なるほど簡単。
この先はいずれということで。
Tuple
Tuple とパターンマッチが組み合わさると確かに便利です。Value Tupleが似てるっていうのもなるほど、ほむり。
let dump x = x.Dump(); x let tuple = (1, "person", "Joeh doe") tuple |> dump let hoge tuple1 = match tuple1 with | (no, kind, name) -> sprintf "No : %A, Kind : %A, Name : %A" no kind name hoge tuple |> dump
名前付きで分解されるの便利ですねぇ。
殴り書きコード
いったんおいておきます。
続き
は、いずれ。
あとは、Maybe とか Option とか Either あたりが話を聞いてて楽しそうだった https://t.co/wTSNpSytLs https://t.co/RXpOELVYmq
— guitarrapc_tech (@guitarrapc_tech) September 3, 2016
クラス、インターフェース、アクティブパターン当りは次回で
pocketberserker.hatenablog.com
追記
むむっ...。
@guitarrapc_tech @kekyo2 冗語構文と軽量構文という2つの書き方のことです。冗語構文は、よりOCamlっぽい書き方になります。https://t.co/cMlVoNS276
— Kentaro Inomata (@matarillo) September 4, 2016
@guitarrapc_tech @matarillo @kekyo2 冗語構文は常に有効で軽量構文は無効化できます。が、 軽量構文が F# の標準です(たぶん・ややこしい)
— はぇ~☆ (@haxe) September 4, 2016
*1:if-else だと条件最後まで読まないとぐんにょりしますからね
*2:sprintf 遅かったけど、 https://stackoverflow.com/questions/16742189/performance-of-sprintf-vs-string-format 3.1 で40x 爆速になったんですね https://t.co/5qlzKeMkg2
*3:便利!
*4:PowerShell やってたせいでしょうが。