F# 8 で追加された便利な機能の紹介

先月 [.NET 8.0](https://dotnet.microsoft.com/ja-jp/download) のリリースがあり、F#もバージョン8が正式リリースされました。
F# 8は大規模なアップデートとなっており、様々な機能が追加されました。
その中で私が特に便利そうだと感じた機能をいくつか紹介します。

## _.Property 記法

### プロパティ取得関数が簡単に書けるようになった！

ある型のプロパティを抽出する関数を書く時に、今までは `fun x -> x.Property` と書く必要がありましたが、もっと短く `_.Property` と書けるようになりました。

例えば以下のような人物リストがあるとします。

```fsharp
type Person = { Name: string; Age: int }
let persons = [
  { Name = "Alice"; Age = 15 }
  { Name = "Bob"; Age = 20 }
  { Name = "Carol"; Age = 25 }
]
```

名前だけを抽出したい場合、従来のF#では以下のように書く必要がありました。

```fsharp
let names = persons |> List.map (fun person -> person.Name)

// 結果
val names: string list = ["Alice"; "Bob"; "Carol"]
```

F#8からは以下のようにもっと短く書くことができます。

```fsharp
let names2 = persons |> List.map _.Name

// 結果
val names2: string list = ["Alice"; "Bob"; "Carol"]
```

この書き方が使えるのは `map` の引数に限定されません。
例えば人物リストの年齢の平均を `averageBy` 関数を使って求める時にも以下のように書けます。
`Age` は `int` 型なのですが `int` 型のままでは平均を取る操作に対応していないので、関数合成 `>>` を使って `Age` の値を `float` 型に変換しています。

```fsharp
let ageAverage = persons |> List.averageBy (_.Age >> float)

// 結果
val ageAverage: float = 20.0
```

そもそもラムダ式の引数以外の箇所でもこの書き方を使うことができます。
以下の例では人物の名前を取得する関数を定義しています。
`_.Name` は `fun x -> x.Name` と同じということを考えれば当然でしょう。

```fsharp
let getName = _.Name

// 結果
val getName: _arg1: Person -> string
```

### filter 系関数でも使いたいけど…

人物リストから成人のみを抽出したい場合、従来のF#では以下のように書きます。

```fsharp
let adultPersons = persons |> List.filter (fun person -> person.Age >= 20)

// 結果
val adultPersons: Person list = [{ Name = "Bob"
                                   Age = 20 }; { Name = "Carol"
                                                 Age = 25 }]
```

これを `_.Property` 形式の書き方で書ければ便利なのですが、残念ながら以下のコードはコンパイルエラーとなります。

```fsharp
let adultPersons2 = persons |> List.filter (_.Age >= 20)

// 結果
error FS0001: この式に必要な型は
    'Person -> bool'
ですが、ここでは次の型が指定されています
    'bool'
```

`_.Property` はあくまで `fun x -> x.Property` の書き換えだということを思い出せば上記がエラーになってしまうことも納得できます。

```fsharp
// こんな書き方はできない
let adultPersons2 = persons |> List.filter ((fun x -> x.Age) >= 20)
```

関数合成を使うことで無理やりこの書き方を使うことができますが、特にコードが短くなっているわけでもないし従来の書き方の方が理解しやすいです。

```fsharp
let adultPersons2 = persons |> List.filter (_.Age >> fun age -> age >= 20)

// 結果
val adultPersons2: Person list = [{ Name = "Bob"
                                    Age = 20 }; { Name = "Carol"
                                                  Age = 25 }]
```

年齢判定部分をラムダ式の代わりに演算子を関数として使うことで短く書けはするのですが、非常に誤解を生みやすいコードになってしまいます。

```fsharp
let adultPersons3 = persons |> List.filter (_.Age >> (<=) 20)
// "(<=) 20" は "fun x -> 20 <= x" と等しい
// 20歳以上なら >= 20 と書ければ理解しやすいのに (<=) 20 と逆向きに書かないといけないので誤解を生みやすい

// 結果
val adultPersons3: Person list = [{ Name = "Bob"
                                    Age = 20 }; { Name = "Carol"
                                                  Age = 25 }]
```

以下のような `flip` 関数を導入することで不等号の向きを直感と一致させることができます。
`flip` の位置に `Age` の値が来ていると考えて読めばすっきり読めると思います。
この `flip` 関数は [FSharpPlus](https://fsprojects.github.io/FSharpPlus/) ライブラリなどでも実装されています。

```fsharp
let inline flip f x y = f y x
let adultPersons4 = persons |> List.filter (_.Age >> flip (>=) 20)

// 結果
val adultPersons4: Person list = [{ Name = "Bob"
                                    Age = 20 }; { Name = "Carol"
                                                  Age = 25 }]
```

## ネストしたレコードのフィールドの変更

F#は「レコード」を使って値オブジェクトを簡単に定義することができます。
上記の `Person` 型もレコードでした。

このレコードのフィールドを別のレコードにしたいケースは非常によく出てきます。
例えば上記 `Person` 型を少し変更して `Name` フィールドを別のレコードにしてみます。

```fsharp
type PersonName = { FirstName: string; FamilyName: string }
type Person = { Name: PersonName; Age: int }

let person = { Name = { FirstName = "John"; FamilyName = "Doe" }; Age = 42 }

// 結果
val person: Person = { Name = { FirstName = "John"
                                FamilyName = "Doe" }
                       Age = 42 }
```

この人物が歳を取ったので年齢を変更したデータが必要になりました。
F#は不変性を重視するので、普通にレコードを定義するとすべてのフィールドが変更不可能となります。
この `Person` 型も既に作成されたインスタンスのフィールド値を変更することはできません。
その代わり、一部のフィールドの値を変更した新しいレコードのインスタンスを簡単に作成する機能があります。

```fsharp
// Age の値だけ変更してその他の値は person のまま維持した新しいインスタンスを作成
let person2 = { person with Age = 43 }

// 結果
val person2: Person = { Name = { FirstName = "John"
                                 FamilyName = "Doe" }
                        Age = 43 }
```

上記の例ではレコードがネストしていない `Age` フィールドを書き換えていたのでシンプルに書けましたが、レコードがネストしている場合、例えば `Name` フィールドの `FamilyName` だけ変更したい場合は従来だと面倒な書き方をする必要がありました。

```fsharp
// Name.FamilyName だけ変更したいのに Name 全体を変更するような書き方が必要だった
let person3 = { person with Name = { person.Name with FamilyName = "Hoge" } }

// 結果
val person3: Person = { Name = { FirstName = "John"
                                 FamilyName = "Hoge" }
                        Age = 42 }
```

F#8からはネストしたレコードについてもシンプルな書き方ができます。
非常に便利ですね！

```fsharp
let person4 = { person with Name.FamilyName = "Hoge" }

// 結果
val person4: Person = { Name = { FirstName = "John"
                                 FamilyName = "Hoge" }
                        Age = 42 }
```

### フィールド名と型名が同じ場合は注意

F#ではフィールド名と型名に同じ名前を使うことができるため、よく同じ名前が使われます。
上記例の `PersonName` 型の名前を `Name` 型に変更してもコンパイルは問題なく通ります。

```fsharp
type Name = { FirstName: string; FamilyName: string }
type Person = { Name: Name; Age: int }

let person5 = { Name = { FirstName = "John"; FamilyName = "Doe" }; Age = 42 }

// 結果
val person5: Person = { Name = { FirstName = "John"
                                 FamilyName = "Doe" }
                        Age = 42 }
```

フィールド名と型名が同じ場合にネストしたレコードのフィールドを変更したい場合、今まで紹介した書き方だとエラーとなってしまいます。

```fsharp
let person6 = { person5 with Name.FamilyName = "Hoge" }

// 結果
error FS0001: この式に必要な型は
    'Person'
ですが、ここでは次の型が指定されています
    'Name'
```

実はこのレコード変更記法では変更したいフィールド名の前にレコードの型名を修飾することができます。
この書き方をすればエラーを回避できます。

```fsharp
let person7 = { person5 with Person.Name.FamilyName = "Hoge" }

// 結果
val person7: Person = { Name = { FirstName = "John"
                                 FamilyName = "Hoge" }
                        Age = 42 }
```

## 他にも便利な機能が追加されています！

今回は紹介しなかった新機能の中にも便利なものがたくさんあります。
個人的には末尾再帰関数かどうかをチェックしてくれる `[<TailCall>]` 属性が追加されたのも嬉しいのですが、筆者の環境（VSCode）ではなぜか警告を出してくれなかったので今回は取り上げませんでした。
他の新機能についても今後紹介したいと考えています。

これを機にF#を書いてみたいって人が増えることを願っています！