🐤

F#の基本のキ

2024/08/13に公開

はじめに

はじめまして、7月にログラスに入社した鈴木(@sunazukin_it)です。

ログラスではドメイン駆動設計(DDD)を導入しています。最近は、関数型プログラミングの一部のテクニックを使っていることもあり、「関数型ドメインモデリング」という本の輪読会[1]を実施しています。

上記の本のサンプルコードはF#で記載されています。日本語の解説がついているため、F#の知識がなくても読み進めることはできます。ですが、技術書を読む際、サンプルコードの文法が気になることはありませんか?私はあります😊

というわけで、今回はサンプルコードをスラスラ読んで関数型ドメインモデリングの理解に集中できるよう、本に出てくるF#の基本的な文法を整理してみました!

※関数型プログラミングの概要については、弊社 山家の記事をご覧ください!
https://zenn.dev/loglass/articles/8133261f5aef92

F#(エフ シャープ)とは

まずはF#とはなにかをみていきます。

F# は、簡潔性、堅牢性、パフォーマンスの高いコードを書くためのユニバーサル プログラミング言語です。F# を使うと、プログラミングの詳細ではなく、問題領域に集中しながら、整理された自己完結型のコードを書くことができます。これを速度と互換性を損なうことなく実現できます。オープンソース、クロスプラットフォームであり、相互運用性があります。

Microsoftの公式HPからの引用です。少しイメージがつきにくいかもしれません。

マイクロソフトが開発した.NET向けのマルチパラダイムプログラミング言語である。Visual Studio 2010より標準開発言語として追加された。

Wikipediaからの引用です。複数のプログラミングパラダイム(例:関数型プログラミング、オブジェクト指向プログラミング)に対応した言語ということですね。

文法

ここからは主な文法をみていきます。

let

letを用いて値を定義することができます。また、:を用いて型の注釈をつけることもできます。

// 値の定義
let x = 1

// 値の定義(型注釈あり)
let y : int = 2

letで定義した値は変更することができません。値を変更したい場合、mutableを付与します。

// 変更可能な値の定義
let mutable z = 1
z <- z + 1  // z=2

val

valを用いることでクラス型または構造体型の値を初期化せずに宣言できます。

type MyClass =
    val a : int  // 初期化せずに宣言できる
    new (a0) = { a = a0; }

let myClassObj = new MyClass(10)  // myClassObj.a=10

関数

let

値と同様に、関数もletを用いて定義することができます。引数を()で囲わないでよいのがポイントです。また、:を用いて引数・戻り値の型注釈をつけることもできます。

// 関数の定義(引数が1個)
let function1 a = a + 1

// 関数の定義(引数が2個)
let function2 a b = a + b + 1

// 関数の定義(引数+戻り値の型の注釈あり)
let function3 (a : int) : int = a + 1

関数を呼び出す際は、関数名の後にスペースを入れ、その後にスペースで区切った引数を指定します。

let x = function1 1    // x=2
let y = function2 1 2  // y=4

関数の内部にローカル変数と関数の本体を含めることができます。また、コンパイラは関数本体の最後の式を使用して戻り値とその型を特定します。

let cylinderVolume radius length =
   // ローカル変数 pi
   let pi = 3.14
   // 最後の式が戻り値となり、型はfloatとなる
   length * pi * radius * radius

ラムダ式

funを用いてラムダ式(匿名関数)を定義することができます。

fun x -> x + 1

ラムダ式はリストなどのコレクションに対して操作を実行する際に活用できます。

// リストの各要素を2倍にする
let list = List.map (fun i -> i * 2) [ 1; 2; 3 ]  // list=[2; 4; 6]

パイプライン

パイプ演算子|>を用いて複数の関数呼び出しを一連の操作として連結することができます。Linuxコマンドの|と似たイメージです。

let function1 a = a * 2
let function2 a = a * 3

// 2倍 * 3倍なので合計6倍
let result = 100 |> function1 |> function2  // result=600

関数合成

>>を用いて関数を合成し、別の関数を生成することができます。パイプラインと少し似ていますが、別の関数を生成する点が大きく異なります。

let function1 a = a * 2
let function2 a = a * 3
let function3 = function1 >> function2

let result = function3 100  // result=600

引数の部分適用

指定されているより少ない数の引数を渡すことで、残りの引数を使用する新しい関数を作成できます。関数型プログラミングの特徴の1つです。

// greeting と name の2つの引数を受け取る関数
let sayGreeting greeting name =	printfn "%s %s" greeting name

// 引数を部分適用して新しい関数を作る
let sayHello = sayGreeting "Hello"
let sayGoodbye = sayGreeting "Goodbye"

sayHello "Alex" // "Hello Alex"が出力される
sayGoodbye "Alex" // "Goodbye Alex"が出力される

Unit型

値が存在しないことを示す型で()によって示されます。C# や C++ などの言語のvoid型と似たイメージです。

型略称・レコード(AND型)・判別共用体(OR型)

コードを読みやすくするために、typeを用いて型略称を型に付けることができます。以下の例だと活用イメージがわかないと思うので、本の「4.3 型の合成」をぜひ読んでください!

type ProductCode = string

type{}を用いて、名前付きの値の集合を表すレコード(AND型)を定義できます。

// Customerという型の値を定義する際に
// First, Last, CustomerIdの3要素をすべて定義する必要がある(AND型)
type Customer = {
  First: string
  Last: string
  CustomerId: int
}

type|を用いて、名前付きケースのうちのいずれかである可能性がある判別共用体(OR型)を定義できます。
ofの後のフィールド名(width, length, radiusなど)は省略可能です

// Shapeという型の値を定義する際に
// Rectangle, Circle, Prismのいずれかの型である必要がある(OR型)
type Shape =
    | Rectangle of width : float * length : float
    | Circle of radius : float
    | Prism of width : float * float * height : float

測定単位

F#の特徴的な言語機能の1つです。プログラミングの学習では、あまりお目にかからない用語ですね!
浮動小数点値と符号付き整数値には、測定単位を関連付けることができます。単位付きの数量を使用することにより、コンパイラで算術関係の単位が正しいことを確認できます。これにより、エラーを防ぐことができます。

// 長さの単位である m, cm を定義
[<Measure>] type m
[<Measure>] type cm

let function1 (x: float<cm>) = x * 2.0
let result = function1 1.5<m>  // cmを指定すべきところでmを指定しているので、コンパイルエラーが発生する

Option

レコード(AND型)や判別共用体(OR型)では値をnullにすることはできませんが、Optionを用いることでデータの欠落・省略を扱えます。

type Customer = {
  First: string
  Last: Option<string>  // 省略可能
  CustomerId: int
}

type Customer = {
  First: string
  Last: string option  // 省略可能(型の後ろで利用することもできる)
  CustomerId: int
}

Result

何らかの処理で失敗が起こりうることを明示的に表現したい場合、Resultを用いて表現することができます。単体でわかりやすい例を示すのが難しいのですが、本の「4.6.2 エラーモデリング」と一緒の例を示します。

// 未払い請求書の支払い処理に関する型の定義
// 未払いの請求書に対して支払い処理を実施する→
//    成功時の出力:支払い済みの請求書
//    失敗時の出力:支払いエラー
type PayInvoice = UnpaidInvoice -> Payment -> Result<PaidInvoice, PaymentError>

コレクション

F#では主に以下のコレクション型があります。要素の区切り文字が;なのがポイントです。

  • list:固定サイズの不変コレクション
  • array:固定サイズの可変コレクション
  • Map:要素を変更できない辞書
// list
let list123 = [ 1; 2; 3 ]

// array
let array1 = [| 1; 2; 3 |]

// Map
let map = Map [ ("key1", "value1"); ("key2", "value2") ]

ループと条件

代表的なものにif...then...else式があります。他にはfor...in式, for...to式, while...do式 などがあります。これは他の言語でも同じような形式なので、馴染みやすいかと思います。

let test x y =
  if x = y then "x equals y"
  elif x < y then "x is less than y"
  else "x is greater than y"

let result = test 20 10  // result="x is greater than y"

パターンマッチング

match...withを用いることで、パターンマッチングによる条件分岐を実現できます。本の中でモデルを実装する際、Result型とパターンマッチングを組み合わせて処理を構築していくケースが多いです。

// 列挙型で型を定義
type Color =
    | Red = 0
    | Green = 1
    | Blue = 2

// 引数で指定された値に応じて出力する内容を変える
let printColorName (color:Color) =
    match color with
    | Color.Red -> printfn "Red"
    | Color.Green -> printfn "Green"
    | Color.Blue -> printfn "Blue"
    | _ -> ()  // 列挙型以外の値が入力されたら何もしない

printColorName Color.Red  // "Red"が表示される

コンピュテーション式

F#の目玉機能であるコンピュテーション式についてです。細かいところまで語るとかなりの分量になってしまうので、今回は触りの部分を紹介します。

定義

F# のコンピュテーション式には、制御フローのコンストラクトとバインドを使用してシーケンス化および結合できる計算を記述するための便利な構文が用意されています。

Microsoftの公式HPから引用したコンピュテーション式の定義です。この文章だけだとイメージがわきにくいので、構文を見ていきましょう。

構文

builder-expr { cexper }

コンピュテーション式の構文は以下の2つから構成されます。

  • builder-expr: コンピュテーションを定義するビルダー型の名前
  • cexper: コンピュテーション式の本体

組み込みのコンピュテーション式

builder-expr に相当する組み込みのコンピュテーション式には、 seq(シーケンス式), async(非同期式), task(タスク式), query(クエリ式) などがあります。
※組み込み以外のコンピュテーション式を独自に定義することも可能です

特殊な構文

コンピュテーション式の本体でのみ使用できる特殊な構文(キーワード)として、let!, and!, do!,yield, yield!, return, return!, match! などがあります。

具体例

今までの構文を使った具体例を挙げます。

コンピュテーション式の具体例

活用例

Result と同じく、単体で活用例を示すのが難しいので、ぜひ本の10章以降を読んでいただければと思います!

参考

おわりに

だいぶ長くなりましたが、以上が関数型ドメインモデリングにでてくるF#の主な文法でした。関数型ドメインモデリングを読む際に参考にしていただければ幸いです!

脚注
  1. VPoEいとひろ(@itohiro73)やDDDを推進している松岡(@little_hand_s)をはじめとする豪華なメンバーと一緒に輪読会を実施できる環境のありがたみを感じています ↩︎

株式会社ログラス テックブログ

Discussion