コンピュテーション式で"はちみーのうた"
コンピュテーション式でキーワード引数 - ぐるぐる~ や カスタムオペレーションの呼び出し順序を制御する - Qiita をみていたら久々にコンピュテーション式で遊びたくなったのでひとネタ供養。
今回のネタは状態遷移を書けると噂の”はちみーのうた”です。 歌詞通りにキーワードを呼び出さないとコンパイルエラーになるはず、きっと。 まぁ多少の妥協があります。
type GotoHachimi = interface end type GotoAshi = interface end type Hachimi = Hachimi with override _.ToString() = "はちみー" interface GotoHachimi type Hachimi<'T when 'T :> GotoHachimi> = Hachimi of 'T with override this.ToString() = match this with | Hachimi t -> sprintf $"{t}はちみー" interface GotoHachimi type Nameru<'T when 'T :> GotoHachimi> = Nameru of 'T with override this.ToString() = match this with | Nameru t -> sprintf $"{t}をなめると" interface GotoAshi interface GotoHachimi type Ashi<'T when 'T :> GotoAshi> = Ashi of 'T with override this.ToString() = match this with | Ashi t -> sprintf $"{t}あしがー" interface GotoAshi type Hayakunaru<'T when 'T :> GotoAshi> = Hayakunaru of 'T with override this.ToString() = match this with | Hayakunaru t -> sprintf $"{t}はやくーなる" interface GotoHachimi type Lyrics = Nameru<Hachimi<Hachimi<Hachimi<Hachimi< Nameru<Hachimi<Hachimi<Hachimi<Hachimi< Hayakunaru<Ashi<Ashi<Ashi<Nameru<Hachimi<Hachimi<Hachimi<Hachimi>>>>>>>> >>>>> >>>>> type HachimiSongBuilder () = member _.Yield(()) = () // 右辺が `Hachimi` だけだと `'T -> Hachimi<'T>` を返してしまうので、 `Hachimi.Hachimi` で固定できるようにしておく [<CustomOperation("はちみー")>] member _.Hachimi(()) = Hachimi.Hachimi [<CustomOperation("はちみー")>] member _.Hachimi(x: #GotoHachimi) = Hachimi(x) [<CustomOperation("をなめると")>] member _.Nameru(x: Hachimi<Hachimi<Hachimi<#GotoHachimi>>>) = Nameru(x) [<CustomOperation("あしがー")>] member _.Ashi(x: #GotoAshi) = Ashi(x) [<CustomOperation("はやくーなる")>] member _.Hayakunaru(x: Ashi<Ashi<Ashi<Nameru<_>>>>) = Hayakunaru(x) member _.Run(song: Lyrics) = printfn $"{song}" let はちみーのうた = HachimiSongBuilder() はちみーのうた { はちみー; はちみー; はちみー はちみー; をなめると あしがー; あしがー; あしがー はやくーなる はちみー; はちみー; はちみー はちみー; をなめると はちみー; はちみー; はちみー はちみー; をなめると }
出力:
はちみーはちみーはちみーはちみーをなめるとあしがーあしがーあしがーはやくーなるはちみーはちみーはちみーはちみーをなめるとはちみーはちみーはちみーはちみーをなめると
- 初回はかならず”はちみー”が来てほしいので単発の
Hachimi
が来るようにYield
とHachimi
メソッドを用意 - カスタムパラメータはoverloadが可能
- 繰り返しを雑に省略定義するために interface を使用
はちみー
を 4回繰り返さないとをなめると
に移動できないように引数の型を調整あしがー
3回も同じ方法
Run
メソッドの引数をLyrics
型に限定することで、歌詞通りなことを保証する- 型パラメータに制約をつけているけど、なくてもたぶん問題ない
久しぶりの記事がこんなのでいいのだろうか :thinking_face:
ZStringを使ったPrintfモジュールを作りたい人生だった
ZStringに組み込まれているものを使ってF#のPrintfモジュールに手が咥えられないか試してみたものの、成果は芳しくなかったので残骸だけ置いておきます。
https://github.com/pocketberserker/FSharp.ZPrintf
一番の問題はUtf16ValueStringBuilder向けのbprintfがうまく動かなかったこと。
https://github.com/pocketberserker/FSharp.ZPrintf/blob/a847d48e26912be2cff5ce516c5f65a7244c9349/src/FSharp.ZPrintf/ZPrintf.fs#L1532 で k
を呼び出す前は値が書き込まれているのに、k
内でBuilderの中身を見ると空っぽになっていてどうして…。
眠くて何か見落としているだけかもしれないので気が向いたら精査するかも。
一応ベンチマーク:
BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18362 Intel Core i7-8650U CPU 1.90GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores .NET Core SDK=3.1.201 [Host] : .NET Core 3.1.3 (CoreCLR 4.700.20.11803, CoreFX 4.700.20.12001), X64 RyuJIT DEBUG DefaultJob : .NET Core 3.1.3 (CoreCLR 4.700.20.11803, CoreFX 4.700.20.12001), X64 RyuJIT
Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
---|---|---|---|---|---|---|---|
StringPlus | 148.3 ns | 1.67 ns | 1.56 ns | 0.0706 | - | - | 296 B |
StringConcat | 167.3 ns | 2.29 ns | 2.14 ns | 0.0880 | - | - | 368 B |
ZStringConcat | 103.3 ns | 1.35 ns | 1.19 ns | 0.0134 | - | - | 56 B |
StringFormat | 172.9 ns | 2.96 ns | 2.77 ns | 0.0305 | - | - | 128 B |
ZStringFormat | 201.7 ns | 1.50 ns | 1.40 ns | 0.0134 | - | - | 56 B |
Printf | 792.6 ns | 8.73 ns | 7.29 ns | 0.1221 | - | - | 512 B |
ZPrintf | 826.8 ns | 6.61 ns | 5.52 ns | 0.1049 | - | - | 440 B |
PrintfList | 347,051.2 ns | 6,814.98 ns | 6,374.74 ns | 15.1367 | - | - | 63477 B |
ZPrintfList | 341,261.4 ns | 3,644.64 ns | 3,409.20 ns | 15.1367 | - | - | 63587 B |
結果的にZStringのソースコード読み漁ったりPrintfモジュールの復習になったので :yoshi:
MessagePack.FSharpExtensions 2.0.0をリリースしました
全国約…何人かわからない F#erの皆様、お待たせしました(お待たせしすぎたかもしれません)。
MessagePack C# v2が正式リリースされてから6か月…さすがにまずいと思ったので重い腰をあげました。 実際は眠れない怒りをぶつけただけなのですけれども。 おそらくこの記事が予約投稿されたときには眠りについているはず。
これやらないとF#が .NET最先端から脱落しますからね!
— neuecc (@neuecc) 2019年12月17日
こういう話はあるものの、自分は特に更新を急いでないからなぁ…と別件にずっとかまけててごめんなさい。 今回の作業は半日かかっていないので、v2がリリースされたときにやっておくべきでした…。
何が変わったのか
- MessagePack C# v2に対応した
- .NET Frameworkとお別れ
- FSharp.Coreを4.7にあげた
- ついでにリポジトリのCIをAppVeyor & TravisCI から GitHub Actions に乗り換え
作業diff: https://github.com/pocketberserker/MessagePack.FSharpExtensions/pull/4
古い環境を窓から投げ捨てる勢いで書き換えました。
動作やAPIは特に変えてません。
とはいえ、大元のMessagePack C# v2が結構変わっているので、MessagePack.FSharpExtensions v1を使っている人(いるのか!?)は書き換えが必要です。
inref
や&
がでてくるあたり、最近の F# っぽい(?)
他のライブラリと異なり、内部APIを引っ張ってきたりしつつILを直書きしている関係で、メンテナンスに気力が必要なんですよね。
判別共用体用のFormatterを生成するDiscriminatedUnionResolver
はDynamicObjectResolver
とDynamicUnionResolver
を悪魔合体させたような存在なので、元ネタがある分楽…ということもなく、同じResolverでStructも捌けるようにしないといけないので注意していないとTypeInitializationErrorの刑に処されます。つらい。
そのかわり、本家と違ってUnityを気にしなくていいのでその分は楽です(その環境が良いかどうかは別の話)。
追加機能をいれるか悩みましたが、何をやるにしても頑張りが必要そうだったので見送ってます。
〆
.NET シリアライズ最前線に追い付けたので一安心。 置いていかれないように引き続きがんば…がん…ががが…
NaNとMap
先日、JavaScriptで以下の挙動になるのはなんでだろうねという話になった。
> m=new Map(); Map {} > m.set(Number.NaN, 0) Map { NaN => 0 } > m.set(Number.NaN, 1) Map { NaN => 1 } > m.set(Number.NaN, 2) Map { NaN => 2 } > Number.NaN === Number.NaN false > Number.NaN !== Number.NaN true
そして理由はMDNに書かれていた。
厳格な等価性では NaN を他のどの値 (自分自身も含む) とも等しくないものとして扱います
https://developer.mozilla.org/ja/docs/Web/JavaScript/Equality_comparisons_and_sameness
NaN は NaN と同じとみなされ (NaN !== NaN であっても)、他の値はすべて === 演算子の意味に従って等価性が考慮されます
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Map
めでたしめでたし…いや、もうちょっとだけ続くんじゃよ。
.NETのDictionaryとF#のMap
ふと、ほかの…例えば.NETのDictionaryやF#のMapはどうなるのか気になったので、Try F#上で試してみた。
open System.Collections.Generic // nanの定義 // https://github.com/dotnet/fsharp/blob/6a8885ff1152db81ab37b94f048a01e88b8847d6/src/fsharp/FSharp.Core/prim-types.fs#L3758 Map.empty |> Map.add nan 1 |> Map.add nan 2 |> printfn "%A" let d = Dictionary<float, int>() d.Add(nan, 1) d.Add(nan, 2) d |> Seq.iter (fun kv -> printfn "%A" kv) nan = nan |> printfn "%b" nan <> nan |> printfn "%b"
map [(NaN, 1); (NaN, 2)] NaN,2 false true
もうちょっと深堀りする必要がありそうだ。
Dictionary
DictionaryはSystem.IEquatable<T>
が実装されていればそれを使うことになっている。
F#におけるfloatは.NETでいうところのdoubleだ。
DoubleはIEquatable<double>
が実装されており、NaN == NaN
はtrueとなるよう実装されている。よってDictionaryでは値が上書きされる。
F# Map
F#のMapのkeyはIComparer<'T>
を使って比較される。
今回はMap.empty
に要素を追加していったので、Map.empty
が用意したLanguagePrimitives.FastGenericComparer<'T>
がcomparerとして使われる。
もっと追ってみる。
GenericComparison
を使っている関数は他にcompare
があるので、こいつで何を返しているか見てみよう。
compare nan nan |> printfn "%d"
1
ということで、別のkey扱いである。
もっと実装を追いたいなら以下を読み進めていけばよいはず。
ちなみに、NaNだとkeyが一致しないので値は取得できない。
let d = Map.empty |> Map.add nan 1 |> Map.add nan 2 // Noneになる d |> Map.tryFind nan |> printfn "%A"
TryFSharpとその技術スタック
今は昔、Web上で簡単にF#を試せるTry FSharpはSilverlight上で動作していました。 しかし世界から光が失われた結果、賢者たちは世界の再構築を余儀なくされたのでした。
というわけでTry FSharpは今もFSharp Software Foundation下で開発、運営されていてしかもモダンなもので構築されているよという話です。 どのくらいモダンかといえば
- Fable
- F# to Javascriptなコンパイラ
- Elmish
- いわゆるElmアーキテクチャのあれ
- Fulma
- CSS FrameworkであるBulmaのfable-react向けbinding
- ちなみにfable-reactはSSRできるよ
聞き覚えのあるものばかりだと思います。 これくらい道具がそろっているとそこそこのウェブサイトであればフルF#で開発できるかも。 いい時代になりましたね。
ちなみにこのTry FSharpはソースコードが公開されています。 興味のある方はぜひ眺めてみましょう。
FableやElmishの話を書き始めるとちょいと長くなりそうなので、この記事では触れずに終わります。 中身がない記事でごめんね、Try FSharpを紹介したかっただけなのさ!
ところで、某FSDNをフル F# でリプレースしようと画策しているのですが、果たしてうまくいくのでしょうか…? うまくいくといいなぁ(未来の自分に期待、そしてフラグ)
F# 4.6の話
忙しくて書く余裕がなかったF# 4.6についてです。といっても機能追加は小粒なのでさくっといきましょう。 間違ってたら指摘をお願いします。
ちなみにこの記事を書いている時点における最新の FSharp.Core は 4.6.2です。
FS-1030
anonymous recordsはC#に存在するanonymous objectsのレコード版ですね。 これまでASP.NET CoreなどのC#製フレームワークをF#で触ったりするとanonymous objectsが前提となっていた部分が煩雑でしたが、これでいい感じに書けるようになります。
FS-1065
4.5で導入されたvalue option用の関数やメソッドが追加されます(なぜ4.5で入らなかったのと突っ込んではいけない)。
あとDebuggerDisplay
属性も付与されるようです。
FS-1066
array, seq, listにtryExactlyOne
関数が追加されます。
コレクション中に要素が1つしかなかった場合にその要素をとりだすexactlyOne
関数のoption版ですね。
おわりに
マイナーバージョンアップなのでこんなものでしょうと思うべきか、もうちょっと頑張ってほしいと思うべきか…それはともかくとして、ちゃんと改善はされていってるのは良い事だでしょう。 RFCsフォルダに未だリリースされていない仕様がたまってきているのでそろそろリリースされないかなぁ。
SpanJson用の F# 拡張ライブラリを作った
SpanJson という新手のJSONシリアライザが今年になって登場していたので、調査がてらに F# 拡張を作りました。
https://github.com/pocketberserker/SpanJson.FSharp.Formatter
https://www.nuget.org/packages/SpanJson.FSharp.Formatter/0.1.0
SpanJson自体が.NETCoreApp 2.1でしか使えない上に F# 向けライブラリなので使う人皆無な気がしていますが、SpanJsonに詳しくなったのでよしとしましょう。
0.1.0で対応しているのは以下の型です。
- option
- voption
- list
- map
- record
- struct record
setは既存実装のままでシリアライズ、デシリアライズできるので自前で用意しませんでした。 判別共用体は例によって必要性がまだわかないので未対応です。 あとはvoption対応のために FSharp.Core を 4.5以上に強制しています、各位新しいバージョンを使っていきましょう。
SpanJsonはUtf8Jsonと似ているようでところどころ違うのが面白いですね、個人的にはUtf8JsonのAPIのほうが好み(というか単純に実装しやすい)と思いましたが。 Resolverの伝搬を型パラメータで行うの結構大変な気がするのですがそこらへんどうなんでしょう…?
あとはSpanJsonでクラスや構造体をシリアライズ、デシリアライズする際はデフォルトかattributeを付けたコンストラクタでなければならない縛りはちょっと地味に厳しいかも? これの関係でrecordはシリアライズできるけどデシリアライズできないという状態になったので、外部ライブラリの型をシリアライズしようとするときに色々書く羽目になるかもしれません。
そういえばこのライブラリを開発する際に C# 7.3 を使ったのですがout var
とか便利ですね…という気持ちになりました。