htsign's blog

ノンジャンルで書くつもりだけど技術系が多いんじゃないかと思います

F# で構造的部分型のメソッドを呼び出す方法

これどっかで書いたような気がしてましたが、少なくともこのブログでは書いたことなかったようなので、すごく久しぶりに生存報告がてら書いてみようと思います。

F# は基本的に公称型で表現される言語なのですが、OCaml を直接の親に持つ言語だからなのか、構造的部分型で表現することも一応は可能です1
例えば、通常は以下のような書き方をします。

type Base() =
    abstract Echo : string -> unit
    default x.Echo text = printfn $"Base ({text})"

type Derived() =
    inherit Base()

    override x.Echo text = printfn $"Derived ({text})"

let echo (x : Base) (text : string) : unit =
    x.Echo text

let () =
    echo (Base()) "hello"    // ==> Base (hello)
    echo (Derived()) "hello" // ==> Derived (hello)

ここでは関数 echo が引数として Base ないしその派生型を要求しており、その Echo を呼び出しています。
BaseEcho が定義されているのだから、それを継承する Derived でも当然 Echo を呼び出すことができるため、正しくコンパイルができますね(C# とかと同じ)

では次の書き方です。

type TypeA() =
    member x.Echo (text : string) : unit = printfn $"TypeA ({text})"

type TypeB() =
    member x.Echo (text : string) : unit = printfn $"TypeB ({text})"

この TypeATypeB は全く別の型であり、何の継承関係もありません。しかし、全く同じシグネチャのメソッドを持っています。
C# にはこの2つの型を同列に扱う仕組みは(たぶん)ありません2。でも F# ならそれができます。この機能は SRTP と呼ばれます。

こう書きます。

type TypeA() =
    member x.Echo (text : string) : unit = printfn $"TypeA ({text})"

type TypeB() =
    member x.Echo (text : string) : unit = printfn $"TypeB ({text})"

let inline echo< ^a when ^a : (member Echo : string -> unit)> (x : ^a) (text : string) : unit =
    (^a : (member Echo : string -> unit) x, text)

let () =
    echo (TypeA()) "hello" // ==> TypeA (hello)
    echo (TypeB()) "hello" // ==> TypeB (hello)

この構造を制約として関数・メソッドの引数に取る方法は MSDN にしっかり書かれています
が、引数に取ったパラメータのメンバーを呼び出す方法はどこにも書かれていません3。なので当時4は調べるのがかなり骨でした。

なお、静的メンバーを制約にすることも可能で、その場合は以下のようになります。

type TypeA =
    static member Echo (text : string) : unit = printfn $"TypeA ({text})"

type TypeB =
    static member Echo (text : string) : unit = printfn $"TypeB ({text})"

let inline echo< ^a when ^a : (static member Echo : string -> unit)> (text : string) : unit =
    (^a : (static member Echo : string -> unit) text)

let () =
    echo<TypeA> "hello" // ==> TypeA (hello)
    echo<TypeB> "hello" // ==> TypeB (hello)

インスタンスメンバーの場合は第一引数にそのインスタンスを、静的メンバーの場合は単純に引数だけを渡してあげればよいです。

静的メンバーを制約としたシグネチャを扱えることで、例えば .NET でよく見る TryParse を統一的に扱うことができます。

let inline tryParseOpt< ^a when ^a : (static member TryParse : string * ^a byref -> bool)> (s : string) : ^a option =
    let mutable x = Unchecked.defaultof< ^a>

    if (^a : (static member TryParse : string * ^a byref -> bool) s, &x) then
        Some x
    else
        None

open System

let () =
    tryParseOpt<int> "123" |> Option.iter (printfn "%d")                            // ==> 123
    tryParseOpt<float> "123.456" |> Option.iter (printfn "%f")                      // ==> 123.456000
    tryParseOpt<DateTime> "05/05/2022 00:00:00+09:00" |> Option.iter (printfn "%A") // ==> 2022/05/05 00:00:00

Unchecked.defaultof は、対象とする型のゼロ値を得る関数ですね。例えば intfloat なら 0 を、配列や System.Uri など参照型なら null を取得します。
この関数は初期化コストが軽いので、とりあえず変数だけ先に宣言しておきたいときによく使います。

さて、ところで型制約と関数本体とに長ったらしい似たような記述を2回もするのダルいですよね。
実は型制約の方の記述は(多少の条件はあるものの)省略できます。

let inline tryParseOpt (s : string) : ^a option =
    let mutable x = Unchecked.defaultof< ^a>

    if (^a : (static member TryParse : string * ^a byref -> bool) s, &x) then
        Some x
    else
        None

こう書けるわけですね。だいぶシンプルになったんじゃないでしょうか。
ただし、こう書くことによって型引数を取れなくなります。

tryParseOpt "123" |> Option.iter (printfn "%d")
tryParseOpt "123.456" |> Option.iter (printfn "%f")

型引数が無くなりましたが、これらはこのままで正しくコンパイルできます。 "%d""%f" で渡されるべき型が明確なので、推論で型が一意に定まるからです。

tryParseOpt "05/05/2022 00:00:00+09:00" |> Option.iter (printfn "%A")

しかしこれはコンパイルエラーになります。理由は明白ですね。 "%A" では渡されるべき型があいまいだからです。
そんなときは型注釈の出番です。

(tryParseOpt "05/05/2022 00:00:00+09:00" : System.DateTime option) |> Option.iter (printfn "%A")

↑のように型を明示することによって正しくコンパイルが通るようになります。

型制約を記述すべきか省略すべきか、どちらがよいかはケースバイケースですが、個人的には付けてしまった方が後々困る場面が少なくなるかなと思います。


余談ですが、 OCaml では元々が構造的部分型なのでこういう記述は非常に簡単明快です。 echo の例を OCaml で書くとこんな感じです。

class type_a =
  object (self)
    method echo (text : string) : unit = Printf.printf "type_a (%s)\n" text
  end

class type_b =
  object (self)
    method echo (text : string) : unit = Printf.printf "type_b (%s)\n" text
  end

let echo (x : < echo : string -> unit; .. >) (text : string) =
  x#echo text

let () =
  echo (new type_a) "hello"; (* ==> type_a (hello) *)
  echo (new type_b) "hello"; (* ==> type_b (hello) *)

なお、 OCaml の型システム的には上記の例の type_atype_b は同じものなので、 < echo : string -> unit; .. > の部分を単に type_a に置き換えても動きます(紛らわしいので避けた方がいいですが)

通常 OCaml ではいちいち型は書かないので5

class type_a =
  object (self)
    method echo text = Printf.printf "type_a (%s)\n" text
  end

class type_b =
  object (self)
    method echo text = Printf.printf "type_b (%s)\n" text
  end

let echo x text =
  x#echo text

let () =
  echo (new type_a) "hello";
  echo (new type_b) "hello";

こんな感じですね。


余談の余談ですが、 < echo : string -> unit; .. > は「 echo : string -> unit と、他にもなんかメンバーを持ってるかもしれない何か」を表わす型です。
今回の例であれば < echo : string -> unit > でもOKですが、その場合は以下のような型を受け取れなくなります( echo 以外のメンバーを持つことが許されなくなるから)

class type_c =
  object (self)
    method echo (text : string) : unit = Printf.printf "type_c (%s)\n" text
    method echo_else : unit = print_endline "type_c (else)"
  end

  1. 公称的部分型や構造的部分型についてはこの記事が分かりやすい。

  2. リフレクションを使って強引に呼ぶことはできますが、受ける型は System.Object になってしまいますし、それゆえに実行時エラーの恐れもあるので非推奨です。

  3. 少なくとも私には見付けられませんでした。

  4. 3~5年くらい前の話

  5. 代わりに .mli ファイルにシグネチャを書きます。F# にも .fsi ファイルというものがありますが、通常手で書くことはないと思います。

Linux版Steam の Portal 2 を日本語環境でプレイする

Pop!_OS 20.10 にて。
日本語でプレイしようとしたら文字が全く表示されない現象に出くわしたため、その解決手順をメモ。

1. fontconfig 用の設定ファイルを作成します。

$ mkdir -p ~/.config/fontconfig
$ vim ~/.config/fontconfig/portal2.conf

2. なんか書きます。

<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
    <match target="pattern">
        <test name="family" qual="any">
            <string>Helvetica</string>
        </test>
        <edit name="family" mode="assign" binding="same">
            <string>Noto Sans CJK JP</string> <!-- ここはお好みで -->
            <string>sans-serif</string>
        </edit>
    </match>
    <dir>/usr/share/fonts</dir>
</fontconfig>

3. Portal 2 の起動オプションに作ったファイルを紐付けます。

FONTCONFIG_FILE=~/.config/fontconfig/portal2.conf %command%

f:id:htsign:20201231050756p:plain

いろいろやったけれども ~/.config/fontconfig/fonts.conf に書く、という方法では上手く動きませんでした。
とりあえず上の手順で表示はされるようになるけれども、 Noto Sans は Helvetica と書体が異なるので、スッキリしない人は似た書体のフォントを探してシステムにインストールし適用すればよいかと思います。

参考:

ノートPCをポチりました

12月からフルリモートとなることが決まりました。
自宅ではどうせ集中できないだろうと思うので、出先で仕事ができるようにとノートPCを買うことにしました。

なので、昨日1は外でも使える *nix がプリインされてるノートPCを物色していたわけです。

初めは Chromebook も有力な選択肢としていました。最近は Linux としても普通に使えるらしいですからね。
が、こいつはOSの特性上メモリがっつり積んでいるモデルが無かったため除外することにしました。

次、 Macbook Pro は性能的には問題ないものの、正直言って Apple は嫌いだし2Macbook Air (2015) 持ってるからいいや、となりました。
触ってて面白くないしね。

となると初めから Linux ディストリビューションが入ったノートが選択肢として上がります。
別にハイエンドな Windows ノートでもいいんですが、 Linux として運用すると考えている以上は Windows のライセンス分無駄なお金を払うことになるし、最初から Linux が入っているって、なんか良くないですか。

で、探してみるんですが、やっぱりまだ世間一般的にはあまりメジャーではないため種類が限られるんですよね。
ないわけではないんですが、少なくとも次の仕事では Docker インスタンスをもりもり立てることが分かっているのでメモリには余裕を持っておきたい。別途 SO-DIMM を購入するのもありだったのかもしれませんが、このときは意識の外でした。

ハイエンドの Linux プリインノートに絞って調べていたら米国コロラド州にある System76 という会社の Lemur Pro というノートを紹介しているブログ記事が目に入りました。
なるほど、これはいい!と記事にあるリンク先に飛んでみたのですが、どうやら Lemur Pro はちょうどCPUの世代交代が近いからなのか買えない様子。
仕方ないから他のを探すか…となったのですが、やっぱり数が少なくてなかなか琴線に触れるのがないんですよね。

立ち戻って System76 のサイトを見返してみると、考えてみたら当然なのですが、ラインナップは Lemur Pro だけではないんですよね。
ラップトップに限っても結構種類があるようです。上から順に見ていったのですが、どれもこれもスペックは十分。
特に WS3 の名を冠するコンピューティングガチ勢もにっこりの3機種はヤベーです。デスクトップ用CPU積んでたりしますし。

私としては、ビルドも走らせるのである程度CPUも強いと嬉しいのですが、一番の重視ポイントはメモリであることを鑑み、性能とコストのバランスから Gazelle を選択しました。

f:id:htsign:20201104001931p:plain

このスペックで18万円弱(送料込み)です。割とお買い得かなと思います。
日本への発送にも対応していますが、無論海外からの通販であるのでハードウェア故障等のサポートが必要な場面ではちょっと面倒になるというリスクはあります。
ただ、情報によるとユーザーによるカスタマイズがしやすいよう、ガワは簡単に開けられるような作りのようです。パーツもある程度は自分で交換できる模様。

Pop!_OS は Ubuntu系列の派生ディストリビューションだそうです。
デスクトップのベースは GNOME とのことで、開発者向けに使いやすいようチューンしてあるんだそうな。

ゲフォ積んでありますが、たぶんあまりゲームには使わないと思います。家にいると集中できない理由の一つがゲームだし、だったら家で仕事しても一緒でしょってなって、ノートの意味とは…みたいな感じになるので。

まだ Placed 状態で組み立てに入っておらず、しばらくは待つ感じになりますが期待しています。


  1. もう日付が変わってしまったので…。

  2. ちなみに Google も割と嫌いです。

  3. WorkStation の略と思われます。

Wandbox のエディタ設定を言語ごとに保存して自動で切り替えする UserScript

表題の通り。
Wandbox さん非常に便利なんですが、言語を切り替えてもエディタ設定は切り替わらないんですよね。

例えば F# はタブ文字をインデントに使えませんが、Go はタブ文字でのインデントを推奨しています。
これら言語を切り替えるときに設定も都度触るのが非常に億劫だと常々思っていました。

というわけで、そのちょっとした不満を解消するために書きました。
設計は「まぁ Proxy 使えば未定義のプロパティも初期化できるやろ」くらいのかなり適当なノリでやってしまったので、メンテが非常につらい感じになっています。
変数の命名も適当です。適当に書いてたら名前が被りそうだったので、普段はオブジェクトをバインドした定数に UPPER_SNAKE_CASE は使わないんですが、他に考えるのも面倒だったので苦し紛れに使っています。

gist.github.com

InoReader 向けに、はてブ integration を追加する UserScript

こんな感じになります。

f:id:htsign:20200511130635p:plain

「動けばいい」をモットーにクソ雑実装。
暇すぎて他にすることなくなったらリファクタリングする可能性が微粒子レベルで存在します。

gist.github.com