F#のパーサーに対する改良が入るかも?

F# Advent Calendar 2020の14日目のエントリーです。

そろそろネタ切れですが、今後入るかもしれない改良の紹介です。

RFC FS-1083

すごい地味なRFCですが、今年のF# Advent CalendarでもあったSRTP(Statically Resolved Type Parameters: 静的に解決される型パラメーター)に関わる改良です。

現状、次のコードはコンパイルできません。

let inline f<^a>: ^a = failwith "error"

現状のF#でコンパイルを通すためには、まず <^ の間に空白を入れ、 < ^a> とする必要があります。 おさまりが悪いので、 > の前にも空白を入れ、 < ^a > などとします。

次に、 >: の間にも空白を入れる必要があります。

つまり、これならコンパイルが通ります。

let inline f< ^a > : ^a = failwith "error"

// 普通の型パラメーターの場合は最初の空白は不要
let g<'a> : 'a = failwith "error"

最初の空白の問題は、 <^ という演算子を定義できるようにするため <^ を一つのトークンとして扱ってしまうのが原因です。 これを、型パラメーターの位置では分割して扱えるようにパーサーを直そう、というのがRFC FS-1083です。

二つ目の問題も >: という一つのトークンとして扱ってしまう点では同じ原因*1ですが、RFC FS-1083の範囲には入っていないようにも見えます。

こういう地味に面倒な問題も解消されていくと嬉しいですね。

*1:なんだけど、実際には「コロンが演算子に含められなくなったよ」という別のエラーに先に引っかかる

nameofの罠

F# Advent Calendar 2020の11日目のエントリーです。

5日目に続いてまた nameof の話です。

nameof にはF#のプログラムとしては当然だけど忘れがちな制約があります。

何の問題もないように見えますが、これはコンパイルエラーです。

let f () = nameof f

これがダメなのは、 ff の定義本体ではまだ見えないのに参照しようとしているからです。

let f x =
  match x with
  | 0 -> 0
  | x -> f (x - 1) + x

これがダメなのと同じ理由ですね。

なので、もしこういうことがしたければ、

let rec f () = nameof f

のように rec を付ける必要があります。 再帰していないのに rec を付けるのは気持ち悪い気もしますが、これは仕方ないでしょう。

ちなみに、メソッドは定義本体で自分自身が見えるので、次のコードはコンパイルが通ります。

type t () =
  static member F() = nameof t.F

ネストしたレコードの更新が楽になるかも?

F# Advent Calendar 2020の9日目のエントリーです。

今日は(も?)、入ると嬉しいRFCの話です。

RFC FS-1049

RFC FS-1049は、 ネストしたレコードのフィールドを with で書き換えられるように拡張しよう、というものです*1

F#のレコードは with を使うことで、一部のフィールドのみを更新した新しいレコードが作れます。

type Customer =
  { Name: string
    Address: string
    Age: int }

// NameとAddressはそのままにAgeをインクリメントする関数
let incrAge customer =
  { customer with Age = customer.Age + 1 }

しかし、ネストしたレコードを更新するためには with もネストして使う必要があります。

例えば、

type A = { X: B }
and B = { Y: C }
and C = { Z: string }

let a = { X = { Y = { Z = "str" } } } 

こういうレコードがあった場合に Z を更新するには、

let a' = { a with X = { a.X with Y = { a.X.Y with Z = "updated" } } }

のようにしなければなりません。

RFC FS-1049は、こう書けるようにしようという提案です。

let a' = { a with X.Y.Z = "updated" }

とてもシンプルになりましたね。

ただこれ、フィールドが option とかだとおそらく結局ネストが必要です。

type A = { X: B }
and B = { Y: C option }
and C = { Z: string }

let a = { X = { Y = Some { Z = "str" } } } 
let a' = { a with X.Y = a.X.Y.map(fun y -> { y with Z = "updated" }) }

まぁ、以前より便利になることは確実なので、入ってほしい拡張ではありますね。

*1:もともとの提案は、「ファーストクラスのLens入れようぜ!」というものだったので大分おとなしいところに着地した感はあります。

退職のお知らせ

今年いっぱいで株式会社オンザロード(以下OTR)を退職することになりました。 有給消化は9月からずっと半休という形で取るという形だったのはちょっと珍しいかもしれません。

OTRに入ったのは2010年1月1日ですから、11年勤めたことになります。 企業のIT化を進める会社(とか開発スタイル)のIT化が当時の自分としては全然進んでいないと感じていたところ、 「そういうこと自分で好きにやっていいよ」と誘われて入社しました。

「少なくともそういう環境が自分の手で作れるまではこの会社にいよう」と思っていたので、 実際に開発面でやりたいことを一通りやった後も留まっていたのは、ふりかえるに次の2点があったからです。

  • 自分にとって理想的なチームで働ける
  • 出来るなら手段は問わないという雰囲気

今回はチーム移籍ということで、1点目の利点は引き続き得られます。 2点目については、OTRにいる中である意味において成長したため、相対的に比重が小さくなっていました。

OTRに入ってからは本当にいろいろなことをやりました。 OTRはSIerなので、当然プロジェクトにアサインされはするのですが、入った経緯もあり入社直後から3~5割程度はプロジェクトに直接関係のないことを自由にやらせてもらいました。 当時はソースコードSubversionで管理していたのですが、それをGitに移行する作業すべてを入社直後からやりました。 そのあとはJenkins(当時Hudson)を導入したり、新規参入者向けのPC選定やキッティング、社内LANの設計から実装、運用など、開発インフラの整備を継続的にやっていきました。

プロジェクトの方でも、技術力を買われて入社したからか、プロジェクトの初期段階から意見を求められることが多く、普通の中小SIerにしてはかなりの無茶を色々としました。 あるプロジェクトで設定ファイルで簡単なループをさせたい、みたいな要件があったときに、最初出てきた設計が1行ずつ解釈していくタイプのインタプリターが出てきたので、 「それやりたいならそれっぽい構文持たせてAST走査するインタプリターにしましょう!」と言ったらあっさり採用されたのでビビりました。 その時はC#だったのでNParsecで実装しました。

また、時間があれば自動化できる部分はないか考え、色々と試行錯誤しました。 その中で、Officeドキュメントの自動生成に関する技術はかなり役に立ちました。 OTRに入ってExcel方眼紙で苦しめられたことはありません。むしろ楽しんで自動化して倒しました。

この「勝手に楽しむ力」みたいなものがOTRで得られたある意味もっとも大きな部分でした。

去年、外部の大規模アジャイル開発のスクラムマスターとして働いたのですが、そこでの経験というか自分の中での気づきがありました。 そこではCOBOLの資産があり、それが大きな意味を持っていたのですが、その時点ではCOBOLなんて読めませんでした。 SMに割り当てられた環境にはVisual Studioは入っておらず、VSCodeしかありませんでした。 しかし、Windows 10にはcsc.exeがあります。 ・・・休憩時間にCOBOLのパーサーを書き始めました。

前職では「ABAPですら無理なのに、COBOLとか絶対無理だわー」と思っていたのですが、 休憩時間に勉強を兼ねてcsc.exe使ってCOBOLのパーサー書いてしかも楽しんでいる自分に気づいたのです。

このとき、「出来るなら手段は問わない雰囲気」が自分の中で急激に重要でなくなりました。 だいたいどんな環境でも勝手に楽しめそうなこと見つけて勝手にやれる、ということに気づいたのです。

この「勝手に楽しむ力」ですが、実にSIer向きの力だとは思います。 それを育ててくれたのはまず間違いなく「出来るなら手段は問わないという雰囲気」だと思っています。 その面でも、OTRは自分にとってとてもいい環境でした。

ですが、今回チーム移籍という形でOTRを離れることになりました。 今の自分にとっては、今のチームはいい環境です。 自分の好きな書籍にMy Job Went To India*1というものがあるのですが、 そのなかで「どんなバンドで演るときも一番下手なプレイヤーでいろ」というジャズギタリストの言葉あります。 この本を読んだときは確かにそうだ、と思ったものですが、今はちょっと違う意見を持っています。

少なくとも今のチームにおいて、「どの分野においても一番上手」な人なんていませんし、同時に「どの分野においても一番下手」な人もいません。 足りないところを補い合えるからこそのチームであり、「自分が一番下手」な分野がある限り大きな学びはあります。 チームという塊で見たときに「下手なこと」がある限り、チーム自体の伸びしろは残っています。 そして今のチームは(その原動力が id:kyon_mm に偏っているという問題はあれど*2 )成長を続けよう、というモチベーションが高いです。

正直な話、自分はSIer向いてると思っているし、ひとりでもやっていける自信はあります。 が、チームとしての成長は一人ではできませんし、同じようなチームをまた最初から作れる自信は全くありません。 なので、実は自分にはOTRをやめる積極的な理由はありません。 きっかけは他のメンバーの選択によるところが大きくはありますが、チームメンバーとして移籍を決めました。

次の職場についてはまた別のエントリーにする予定です。 チーム移籍活動自体の話も書く予定ではいます。

例のリストも貼っておきます。

次の職場でもがんばります!

*1:今は情熱プログラマーという微妙な名前になってしまった。この題の方がよくないですか?ちなみにサブタイトルは「オフショア時代のソフトウェア開発者サバイバルガイド」です。こっちもいい。

*2:問題があるということは改善の余地があるということですね!

コンピュテーション式の壊れた部分がなおりそう

F# Advent Calendar 2020の6日目のエントリーです。

今日は将来のF#の話です。

RFC FS-1087

RFC FS-1087は3つのFeatureをまとめたRFCとなっています。

  • Tasks
  • Resumable state machines
  • Respecting Zero methods in computation expressions ひとつめはコンピュテーション式で Task が扱えるビルダーを追加しましょうという話です。 これは特に面白みはありません。

ふたつめはF# vNextは何が "ヤバい" のか: Monadic Programming の新時代という素晴らしいエントリーで言及されている話で、 とてもワクワクする話ですが、今回はこれの話ではありません。

みっつめの「Respecting Zero methods in computation expressions」についてです。

Respecting Zero methods in computation expressions

これは4年前に書いたComputation Expression Problemの解決です。

RFCではこうあります。

このRFC以前では、 do! expr がコンピュテーション式の最後の位置にある場合、次のように処理された。

  1. Return メソッドがある場合、 builder.Bind(expr, fun () -> builder.Return())
  2. そうでない場合、 builder.Bind(expr, fun () -> builder.Zero())

新しいルールは追加の条件を持ち、次のようになる。

  1. Return メソッドがあり、DefaultValue 属性が付いた Zero メソッドがない場合、 builder.Bind(expr, fun () -> builder.Return())
  2. そうでない場合、 builder.Bind(expr, fun () -> builder.Zero())

つまり、 DefaultValue 属性付きの Zero メソッドがある場合、今までは Return に変換されていたのが Zero に変換されるようになるわけです。

DefaultValue 属性を使わない場合は既存の挙動と同じになるというのはうまい回避方法だと思います。

これによって、より使いやすいコンピュテーション式ビルダーが提供できるようになるでしょう。 将来の楽しみが増えますね!

nameofについて

F# Advent Calendar 2020の5日目のエントリーです。

F#5.0で導入された nameof は便利なんですが、いくつか制限があります。

まず、 nameof はファーストクラス関数ではありません。 そのため、

  • let f = nameof はできない
  • x |> nameof はできない

のような制限があります。

ひとつめは出来たら面白いですが、もし実装しようとすると関数呼び出しのたびに実体が nameof かどうか見ないといけなくなりそうで、 おそらく実現されることはないでしょう。

ふたつめは例えばinline関数のみに限って緩和されるようなことはあるかもしれませんが、どうなんでしょうか。

他にも、昨日のエントリーで紹介した nameof パターンにも制限があります。

nameofnameof symbolnameof<'T> の2つの形式がありますが、 nameof パターンで使えるのは前者のみです。

// こうは書けない
let f<'a> = function
| nameof<'a> -> "yes"
| _ -> "no"

おまけ

nameof はファーストクラスの関数ではないのは上で紹介した通りですが、実はシンボルではあります。 そのため、 nameof nameof は合法で、 "nameof" が返されます。

また、演算子もシンボルなので、 nameof (+) のようにすれば "+" が返されます。 面白いですね。

F# 5.0の新機能 その2

F# Advent Calendar 2020の1日目の記事であるF# 5.0の新機能で大体書かれているのですが、2つ抜けているものがあるので紹介します。

nameofパターン

RFC FS-1085

nameof の値でパターンマッチできます。

RFCにはタグを nameof で取り出しておいて、それを元にデシリアライズする例が載っています。

// 使う型

type RecordedEvent = { EventType: string; Data: byte[] }

type MyEvent =
  | A of AData
  | B of BData
// シリアライズ用関数

let serialize (e: MyEvent) : RecordedEvent =
  match e with
  | A adata -> { EventType = nameof A; Data = JsonSerializer.Serialize<AData>(adata) }
  | B bdata -> { EventType = nameof B; Data = JsonSerializer.Serialize<BData>(bdata) }
// デシリアライズ用関数

let deserialize (e: RecordedEvent) : MyEvent =
  match e.EventType with
  | nameof A -> A (JsonSerializer.Deserialize<AData>(e.Data))
  | nameof B -> B (JsonSerializer.Deserialize<BData>(e.Data))
  | t -> failwithf "Invalid EventType: %s" t

地味に便利ですね。

string関数の実装変更

RFC FS-1089

string は今まで静的に解決される型パラメーターを取るinline関数だったのですが、F#5.0からは普通の型パラメーターを取るinline関数になります。 また、今までデッドコードだったコードを有効化し、より多くの型に対するケースを追加しています。

これは例を見てもらうとびっくりするかもしれません。

// 今まではエラーだったが、F#5.0からはエラーにならない
type Either<'L, 'R> =
  | Left of 'L
  | Right of 'R
  override this.ToString() =
    match this with
    | Left x -> string x
    | Right x -> string x

他にもフォーマットに "g" を指定したのを null に変更したりもしています。 diffを見てもらった方が分かりやすいかもしれません。