コンピュテーション式で"はちみーのうた"

コンピュテーション式でキーワード引数 - ぐるぐる~カスタムオペレーションの呼び出し順序を制御する - 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 が来るように YieldHachimi メソッドを用意
  • カスタムパラメータは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#L1532k を呼び出す前は値が書き込まれているのに、k 内でBuilderの中身を見ると空っぽになっていてどうして…。 眠くて何か見落としているだけかもしれないので気が向いたら精査するかも。

一応ベンチマーク:

https://github.com/pocketberserker/FSharp.ZPrintf/blob/a847d48e26912be2cff5ce516c5f65a7244c9349/examples/PerfBenchmark/Program.fs

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をリリースしました

www.nuget.org

全国約…何人かわからない F#erの皆様、お待たせしました(お待たせしすぎたかもしれません)。

MessagePack C# v2が正式リリースされてから6か月…さすがにまずいと思ったので重い腰をあげました。 実際は眠れない怒りをぶつけただけなのですけれども。 おそらくこの記事が予約投稿されたときには眠りについているはず。

こういう話はあるものの、自分は特に更新を急いでないからなぁ…と別件にずっとかまけててごめんなさい。 今回の作業は半日かかっていないので、v2がリリースされたときにやっておくべきでした…。

何が変わったのか

作業diff: https://github.com/pocketberserker/MessagePack.FSharpExtensions/pull/4

古い環境を窓から投げ捨てる勢いで書き換えました。

動作やAPIは特に変えてません。 とはいえ、大元のMessagePack C# v2が結構変わっているので、MessagePack.FSharpExtensions v1を使っている人(いるのか!?)は書き換えが必要です。 inref&がでてくるあたり、最近の F# っぽい(?)

他のライブラリと異なり、内部APIを引っ張ってきたりしつつILを直書きしている関係で、メンテナンスに気力が必要なんですよね。 判別共用体用のFormatterを生成するDiscriminatedUnionResolverDynamicObjectResolverDynamicUnionResolver悪魔合体させたような存在なので、元ネタがある分楽…ということもなく、同じ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

DictionarySystem.IEquatable<T>が実装されていればそれを使うことになっている。

F#におけるfloatは.NETでいうところのdoubleだ。 DoubleはIEquatable<double>が実装されており、NaN == NaNはtrueとなるよう実装されている。よってDictionaryでは値が上書きされる。

https://github.com/microsoft/referencesource/blob/a7bd3242bd7732dec4aebb21fbc0f6de61c2545e/mscorlib/system/double.cs#L147

https://github.com/dotnet/runtime/blob/221f869e9bac3cafcfe6bd35d062e2fbfe8accba/src/libraries/System.Runtime/tests/System/DoubleTests.cs#L111

F# Map

F#のMapのkeyはIComparer<'T>を使って比較される。

https://github.com/dotnet/fsharp/blob/6a8885ff1152db81ab37b94f048a01e88b8847d6/src/fsharp/FSharp.Core/map.fs#L123

今回はMap.emptyに要素を追加していったので、Map.emptyが用意したLanguagePrimitives.FastGenericComparer<'T>がcomparerとして使われる。

https://github.com/dotnet/fsharp/blob/6a8885ff1152db81ab37b94f048a01e88b8847d6/src/fsharp/FSharp.Core/map.fs#L466

もっと追ってみる。

https://github.com/dotnet/fsharp/blob/78bb83aca6cba3d85dee111211d4c0c99a37595d/src/fsharp/FSharp.Core/prim-types.fs#L2177

https://github.com/dotnet/fsharp/blob/78bb83aca6cba3d85dee111211d4c0c99a37595d/src/fsharp/FSharp.Core/prim-types.fs#L2088

GenericComparisonを使っている関数は他にcompareがあるので、こいつで何を返しているか見てみよう。

https://github.com/dotnet/fsharp/blob/78bb83aca6cba3d85dee111211d4c0c99a37595d/src/fsharp/FSharp.Core/prim-types.fs#L3293

compare nan nan |> printfn "%d"
1

ということで、別のkey扱いである。

もっと実装を追いたいなら以下を読み進めていけばよいはず。

https://github.com/dotnet/fsharp/blob/78bb83aca6cba3d85dee111211d4c0c99a37595d/src/fsharp/FSharp.Core/prim-types.fs#L2159

https://github.com/dotnet/fsharp/blob/78bb83aca6cba3d85dee111211d4c0c99a37595d/src/fsharp/FSharp.Core/prim-types.fs#L1954

https://github.com/dotnet/fsharp/blob/78bb83aca6cba3d85dee111211d4c0c99a37595d/src/fsharp/FSharp.Core/prim-types.fs#L1097

https://github.com/dotnet/fsharp/blob/78bb83aca6cba3d85dee111211d4c0c99a37595d/src/fsharp/FSharp.Core/prim-types.fs#L1080

ちなみに、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 F#

というわけでTry FSharpは今もFSharp Software Foundation下で開発、運営されていてしかもモダンなもので構築されているよという話です。 どのくらいモダンかといえば

聞き覚えのあるものばかりだと思います。 これくらい道具がそろっているとそこそこのウェブサイトであればフルF#で開発できるかも。 いい時代になりましたね。

ちなみにこのTry FSharpはソースコードが公開されています。 興味のある方はぜひ眺めてみましょう。

GitHub - fsharp/TryFSharp

FableやElmishの話を書き始めるとちょいと長くなりそうなので、この記事では触れずに終わります。 中身がない記事でごめんね、Try FSharpを紹介したかっただけなのさ!

ところで、某FSDNをフル F# でリプレースしようと画策しているのですが、果たしてうまくいくのでしょうか…? うまくいくといいなぁ(未来の自分に期待、そしてフラグ)

F# 4.6の話

忙しくて書く余裕がなかったF# 4.6についてです。といっても機能追加は小粒なのでさくっといきましょう。 間違ってたら指摘をお願いします。

ちなみにこの記事を書いている時点における最新の FSharp.Core は 4.6.2です。

FS-1030

https://github.com/fsharp/fslang-design/blob/7240c64de3e85f69de872b1f0a7db4d91f7a7e1a/FSharp-4.6/FS-1030-anonymous-records.md

anonymous recordsはC#に存在するanonymous objectsのレコード版ですね。 これまでASP.NET CoreなどのC#フレームワークをF#で触ったりするとanonymous objectsが前提となっていた部分が煩雑でしたが、これでいい感じに書けるようになります。

FS-1065

https://github.com/fsharp/fslang-design/blob/7240c64de3e85f69de872b1f0a7db4d91f7a7e1a/FSharp.Core-4.6.0/FS-1065-valueoption-parity.md

4.5で導入されたvalue option用の関数やメソッドが追加されます(なぜ4.5で入らなかったのと突っ込んではいけない)。 あとDebuggerDisplay属性も付与されるようです。

FS-1066

https://github.com/fsharp/fslang-design/blob/7240c64de3e85f69de872b1f0a7db4d91f7a7e1a/FSharp.Core-4.6.0/FS-1066-tryExactlyOne.md

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とか便利ですね…という気持ちになりました。