6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

F#Advent Calendar 2023

Day 25

F# 8で追加された「_.Property 記法」の深掘り

Last updated at Posted at 2023-12-25

_.Property 記法について

F# 8 で追加された便利な機能の紹介 でも記載しましたが、F# 8 _.Property 記法というものが追加されました。
個人的にはこの _.Property 記法がF# 8での変更点の中で最も影響力が大きいと感じたので、さらにこの記法について深掘りしてみます。

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

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

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

この人物リストから名前だけを抽出して名前のリストに変換したい場合、従来のF#では以下のように書く必要がありました。

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

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

F#8からは _.Property 記法を使ってもっと短く書くことができます。

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

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

_.Property 記法を最もよく使うシチュエーションは上記のようにコレクションの map 関数に渡す変換関数だと思います。
map 関数以外でも、何らかの変換を行うための関数では同じように使うことができます。
例えばすべての人物の年齢を合計したい場合は sumBy 関数を使って以下のように書くことができます。

let sumOfAge = persons |> List.sumBy _.Age

// 結果
val sumOfAge: int = 60

_.Property 記法で書けるのはプロパティへのアクセスだけでなく、メソッド呼び出しも可能です。
しかもメソッド呼び出しの場合にそれを囲むカッコは必要ありません。便利ですね!

let personStrings = persons |> List.map _.ToString()

// 結果
val personStrings: string list =
  ["{ Name = "Alice"
  Age = 15 }"; "{ Name = "Bob"
  Age = 20 }";
   "{ Name = "Carol"
  Age = 25 }"]

_.Property の意味は fun x -> x.Property だと冒頭に書きましたが、つまり _.Property とは関数なわけです。
_.Property と関数合成することで抽出した値に対する追加処理を書くことができます。

例えばすべての人物の名前を抽出して「さん付け」にしたい場合、以下のように書くことができます。

let names3 = persons |> List.map (_.Name >> sprintf "%sさん")

// 結果
val names3: string list = ["Aliceさん"; "Bobさん"; "Carolさん"]

_.Property 記法の正体から考える応用的な使い方

filter 関数で使いたい場合

_.Property 記法を filter 関数で使いたいと思った時、以下のように書けそうな気がしますがこれはコンパイルエラーとなってしまいます。

let adultPersons = persons |> List.filter (_.Age >= 20)

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

_.Property の意味は fun x -> x.Property を表す関数だということに立ち戻って考えると、上記のコードは確かにおかしい表現になっています。
filter 関数で _.Property 記法を使いたければ、関数合成を使って以下のように書くことになります。

// 取り出したAgeプロパティの値を比較するためのラムダ式と関数合成。長くなってあまり意味がない…
let adultPersons2 = persons |> List.filter (_.Age >> fun age -> age >= 20)
// 演算子を関数のように使うことで短く書けますが、不等号の向きが直感的ではなくて読みづらい…
let adultPersons3 = persons |> List.filter (_.Age >> (<=) 20)

上記のどちらの書き方をしても微妙なのですが、以下のような引数の順序を入れ替えるための flip 関数を用意することでより直感的に書けるようになります。
この flip 関数は FSharpPlus ライブラリにも用意されています。

let inline flip f x y = f y x
// flip の箇所は前の関数の結果が入る変数だと解釈して読むことで自然に読むことができます
let adultPersons4 = persons |> List.filter (_.Age >> flip (>=) 20)

filter 関数と相性が良くない _.Property 記法ですが、このような工夫をすることで filter 関数でも使いやすくなります。

fold 関数で使いたい場合

実は _.Property 記法は map 関数だけでなく fold 関数とも相性が良いです。

例えば文字列に含まれる改行 \r\n\n を両方ともスペースに置換したい場合、fold 関数と _.Property 記法を使って以下のように書けます。

let originalString = "Hello,\nWorld!\r\n"
let replacingPatterns = ["\r\n", " "; "\n", " "]
let replacedString = (originalString, replacingPatterns) ||> List.fold _.Replace

// 結果
val replacedString: string = "Hello, World! "

なぜこのような書き方が可能なのかは、以下のように少しずつ省略されている要素を補完していくことで理解できます。

// _.Property記法をラムダ式に戻してみました。これではまだ意味が理解しにくいかもしれません
let replacedString2 =
  (originalString, replacingPatterns)
  ||> List.fold (fun acc -> acc.Replace)
// さらに省略されているReplaceへの引数を明示的に追加してみます。これで理解しやすくなります
let replacedString3 =
  (originalString, replacingPatterns)
  ||> List.fold (fun acc (oldStr, newStr) -> acc.Replace(oldStr, newStr))

fun acc (oldStr, newStr) ->(oldStr, newStr) はタプルで acc.Replace(oldStr, newStr)(oldStr, newStr) は多変数引数ですが、F#では多変数引数にタプルをそのまま渡すことが可能なので、タプルを渡す部分を丸ごと省略して書くことができます。
さらにそれを _.Property 記法に変換したのが最初の List.fold _.Replace だったわけです。

このように fold 関数のアキュムレータ部分(上記の例だと acc)のメソッドを繰り返し呼び出したい場合に _.Property 記法がバッチリハマる場合があります。

_.Property 記法の制約

_ だけで使うことはできない

_.Property 記法の元ネタであろう Scala では、ラムダ式の引数そのものを _ で表現することができます。

// 1を足す関数
val plus1: Int => Int = 1 + _

しかしF#の _.Property 記法では上記の例のように _ だけで引数そのものを表現することはできません。必ず _.Property_.Method(a) のようにプロパティやメソッドなどの呼び出しが必要となります。

なお、上記のような関数をF#で書きたい場合は以下のように書くことができます。

let plus1 = (+) 1

2引数関数で両方の引数を _.Property 記法で書くことはできない

例えば map2 関数の両方の引数に対してプロパティアクセスした結果を処理したいからといって、以下のような書き方はできません。

let xs = [1; 2; 3]
let ys = [4; 5; 6]
let zs = (xs, ys) ||> List.map2 (_.ToString() + _.ToString())

// 結果
// error FS0002: この関数の引数が多すぎるか、関数を使用できない場所で関数が使用されています

直接インデクサを呼び出すことはできない

例えば以下のような配列の要素を呼び出したいコードはコンパイルエラーとなります。

let array = [| 1; 2; 3 |]
let firstElement = array |> _[0]

// 結果
// error FS0010: 予期しない シンボル '[' です 式内。'.' または他のトークンを指定してください。

F# の古いインデックスアクセス記法として array.[0] のように . をつけて書く記法もあるので、こちらなら . がついてるから大丈夫なのかと思いきや、こちらの記法ではコンパイルエラーにこそならないものの意図しない動作をします。

let array = [| 1; 2; 3 |]
let firstElement2 = array |> _.[0]

// 結果
val firstElement2: int list = [0]

なぜか [0] という1要素だけのリストが返ってきてしまいました。
このように直接インデクサを呼び出すことはできません。

なお、プロパティアクセスした先のインデクサを呼び出すことは可能です。

let hoge = {| Foo = [| 1; 2; 3 |] |}
let firstElementOfHoge = hoge |> _.Foo[0]

// 結果
val firstElementOfHoge: int = 1

_.Property 記法によって変わるF#の世界

従来のF#では、F#の特徴と言ってもいいパイプライン演算子を使った記法とプロパティアクセスやメソッドアクセスの相性が悪く、特にF#のレコードやC#のクラスのようなプロパティを持つデータクラスを処理するコードが綺麗に書けないという欠点がありました。
中にはF#で書きやすくするために、以下のようなヘルパー関数を用意したりしていました。

type Person = { Name: string; Age: int }
module Person =
  // ヘルパー関数
  let name person = person.Name
  let age person = person.Age

let persons = [
  { Name = "Alice"; Age = 15 }
  { Name = "Bob"; Age = 20 }
  { Name = "Carol"; Age = 25 }
]
let names = persons |> List.map Person.name

_.Property 記法が導入されたことでF#のレコードやC#のクラスとの親和性が一気に上がると考えられます。

また、従来は型のメソッドを書いてもアクセスがしにくかったので、ある型に対する処理はなるべくモジュール内の関数として定義するのが主流でした。

module Person =
  // とても良く使う処理なのでプリミティブに持たせてもよい場合でも、今まではメソッドアクセスが面倒だったので関数にしていた
  let getNameWithRespection person = person.Name + "さん"

let names4 = persons |> List.map Person.getNameWithRespection

// 結果
val names4: string list = ["Aliceさん"; "Bobさん"; "Carolさん"]

F#8からは _.Property 記法が追加されたことで、プリミティブにしてもよい処理であればメソッドとして定義することが増えそうです。

// これからはプリミティブに持たせたい処理はメソッドとして書いてもよさそう
type Person = { Name: string; Age: int }
with
  member this.NameWithRespection = this.Name + "さん"

let names5 = persons |> List.map _.NameWithRespection

// 結果
val names5: string list = ["Aliceさん"; "Bobさん"; "Carolさん"]

_.Property 記法は翻ってF#の強みであるパイプライン演算子による記法をより強化することになります。
使うべきところでどんどん使っていきたいですね。

6
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?