値・型・バインディング
基礎let で値をバインド(デフォルトは不変)。mutable キーワードを付けると変更可能。<- で再代入する。
let x = 42 // 不変バインディング
let mutable count = 0 // ミュータブル
count <- count + 1 // 再代入(<- を使う)
let pi: float = 3.14 // 型注釈F# の基本型: int・float・bool・string・char・unit(())。.NET の型と互換(int は System.Int32)。数値リテラルには型サフィックスがある。
let i: int = 42
let f: float = 3.14
let b: bool = true
let s: string = "F#"
let u: unit = () // void 相当
let c: char = 'A'
let l: int64 = 100L // サフィックスで型指定F# は Hindley–Milner 型推論を採用しており、ほぼすべての型を自動推論できる。型注釈は省略可能で、ジェネリック型も自動で推論される。
let add a b = a + b // a, b: int と推論
let addF (a: float) b = a + b // float と明示
let len xs = List.length xs // 'a list -> intunit は副作用のみを持つ処理の戻り値型。printfn は unit を返す。F# では副作用を型で明示的に扱う。
printfn "Hello, %s!" "F#" // unit を返す
let greet name: unit = printfn "Hi, %s" name
ignore (1 + 1) // 値を捨てて unit にする制御フロー
基礎F# の if は式であり値を返す。両分岐の型が一致する必要がある。
let max a b = if a > b then a else b
let label =
if score >= 90 then "A"
elif score >= 70 then "B"
else "C"F# のパターンマッチ。型・値・タプル・リスト・レコード・判別共用体などあらゆる構造でマッチできる。
let describe x =
match x with
| 0 -> "zero"
| n when n < 0 -> sprintf "negative %d" n
| n -> sprintf "positive %d" n命令型のループ構文。関数型スタイルでは List.map/List.iter などを優先するが、for は手続き的なコードに使う。
for i in 1..10 do
printfn "%d" i
for i in 10 .. -1 .. 1 do // downto
printfn "%d" i
let mutable i = 0
while i < 5 do
printfn "%d" i
i <- i + 1do ブロック内では複数の式を改行(またはセミコロン)で区切って順次実行できる。
let processFile path =
let content = System.IO.File.ReadAllText(path)
printfn "Read %d chars" content.Length
content.ToUpper()関数
基礎F# の関数は自動でカリー化される。複数引数の関数は実際には「引数を1つとって関数を返す関数」の連鎖。
let add a b = a + b // int -> int -> int
let add5 = add 5 // 部分適用 → int -> int
add5 3 // 8
// 無名関数
let double = fun x -> x * 2
let triple = (*) 3 // 演算子も部分適用可|> で左辺の値を右辺の関数の最後の引数に渡す。関数合成 >> で関数を連結できる。
"hello world"
|> String.split ' '
|> List.map (fun s -> s.[0..0].ToUpper() + s.[1..])
|> String.concat " "
// "Hello World"
let processText = String.trim >> String.toLower >> String.length再帰関数には let rec を使う。末尾再帰は acc 引数パターンで書くとコンパイラが最適化する。
let rec factorial n =
if n <= 1 then 1 else n * factorial (n - 1)
// 末尾再帰版
let factorial' n =
let rec loop acc = function
| 0 | 1 -> acc
| n -> loop (acc * n) (n - 1)
loop 1 nfun キーワードで無名関数(ラムダ)を定義。List.map・List.filter・List.fold などに渡して使う。
[1..5] |> List.map (fun x -> x * x) // [1;4;9;16;25]
[1..10] |> List.filter (fun x -> x % 2 = 0) // [2;4;6;8;10]
[1..5] |> List.fold (+) 0 // 15リスト・配列・シーケンス
基礎F# リストは不変の連結リスト。[1;2;3] または 1 :: [2;3]。@ で連結。List モジュールで操作。
let xs = [1; 2; 3] // セミコロン区切り
let ys = 0 :: xs // [0;1;2;3](先頭追加)
let zs = xs @ [4; 5] // [1;2;3;4;5](結合)
let h, t = List.head xs, List.tail xs[|1;2;3|] でミュータブルな配列を定義。インデックスアクセスは arr.[i](または arr[i])。Array モジュールで操作。
let arr = [| 1; 2; 3 |]
arr.[0] // 1
arr.[1] <- 99 // 変更可能
Array.map ((*) 2) arr // [|2;198;6|]seq { ... } で遅延評価シーケンス。IEnumerable と互換。無限シーケンスも表現できる。
let nats = Seq.initInfinite id // 0,1,2,...
nats |> Seq.take 5 |> Seq.toList // [0;1;2;3;4]
let evens = seq { for i in 0..2..100 -> i }[ for x in ... -> expr ] で内包表記によるリスト生成。if でフィルタリング。
let squares = [ for i in 1..10 -> i * i ]
// [1;4;9;16;25;36;49;64;81;100]
let evens = [ for i in 1..20 do if i % 2 = 0 then yield i ]文字列・フォーマット
基礎F# の文字列は .NET の System.String。sprintf で型安全なフォーマット。String モジュールで操作。
let s = "Hello, F#!"
s.Length // 10
s.ToUpper() // "HELLO, F#!"
String.concat ", " ["a";"b";"c"] // "a, b, c"
sprintf "Pi is %.4f" System.Math.PI // "Pi is 3.1416"F# 5 以降は $"..." で文字列補間が使える。型安全でコンパイル時チェックが働く。
let name = "World"
let greeting = $"Hello, {name}!" // "Hello, World!"
let info = $"Pi = {System.Math.PI:.4f}" // "Pi = 3.1416"型安全なフォーマット関数群。コンパイル時に書式文字列の型をチェックする。%d(整数)%s(文字列)%f(浮動小数点)%A(任意型)。
printf "%s is %d years old
" "Alice" 30
printfn "%A" [1; 2; 3] // [1; 2; 3](デバッグ表示)
let msg = sprintf "score: %d" 95例外処理
基礎try...with で例外を捕捉、try...finally でクリーンアップ。パターンマッチで例外の種類を区別できる。
try
let n = int "abc"
n
with
| :? System.FormatException as e -> printfn "Format error: %s" e.Message; 0
| ex -> printfn "Unknown: %s" ex.Message; -1Result<'T,'TError> で成功(Ok)か失敗(Error)を表す。Option<'T> は Some/None。パターンマッチで処理する関数型スタイル。
let divide a b =
if b = 0 then Error "Division by zero"
else Ok (a / b)
match divide 10 2 with
| Ok result -> printfn "Result: %d" result
| Error msg -> printfn "Error: %s" msgfailwith は System.Exception を発生させる便利関数。invalidArg は引数エラー用。F# では Result を使うのが推奨スタイル。
let head = function
| [] -> failwith "Empty list"
| x :: _ -> x
let checkAge age =
if age < 0 then invalidArg "age" "Must be non-negative"
ageレコードと判別共用体
基礎レコードは名前付きフィールドを持つ不変データ構造。with キーワードでコピー生成(構造的等値性も自動)。
type Person = {
Name: string
Age: int
}
let alice = { Name = "Alice"; Age = 30 }
let older = { alice with Age = 31 } // コピー
alice = { Name = "Alice"; Age = 30 } // true(構造的等値)代数的データ型(ADT)。type Shape = Circle of float | Rectangle of float * float のように定義し、パターンマッチで処理する。型安全な条件分岐。
type Shape =
| Circle of radius: float
| Rectangle of width: float * height: float
| Triangle of base: float * height: float
let area = function
| Circle r -> System.Math.PI * r * r
| Rectangle(w, h) -> w * h
| Triangle(b, h) -> b * h / 2.0Option<'T> は Some value または None。null の代替。Option.map・Option.bind・Option.defaultValue で関数型スタイルの操作。
let safeDiv a b =
if b = 0 then None else Some (a / b)
safeDiv 10 2 |> Option.map ((*) 3) // Some 15
safeDiv 10 0 |> Option.defaultValue -1 // -1ケースが1つの判別共用体で型安全なラッパーを定義。UserId と OrderId を int で区別できる。
type UserId = UserId of int
type OrderId = OrderId of int
let uid = UserId 42
let (UserId rawId) = uid // アンラップ
// let wrong: OrderId = uid // コンパイルエラークラスとインターフェース
基礎.NET との互換のためクラスも定義できる。type ClassName(args) = が基本構文。member this.Method でインスタンスメソッドを定義。
type Counter(initial: int) =
let mutable count = initial
member _.Value = count
member _.Increment() = count <- count + 1
member _.Reset() = count <- 0
let c = Counter(10)
c.Increment()
printfn "%d" c.Value // 11.NET インターフェースを実装できる。interface IFoo with で明示的実装。
type IShape =
abstract Area: float
type Circle(r) =
interface IShape with
member _.Area = System.Math.PI * r * r
let s: IShape = Circle(5.0)
printfn "%.2f" s.Area[ アトリビュートでスタック割り当ての値型を定義。ヒープ割り当てを避けてパフォーマンスを改善できる。
[<Struct>]
type Vector2D = { X: float; Y: float }
let v1 = { X = 1.0; Y = 2.0 }
let v2 = { X = 3.0; Y = 4.0 }
let sum = { X = v1.X + v2.X; Y = v1.Y + v2.Y }ジェネリクス・型制約
応用型変数は 'a・'T で表記。F# は自動汎化により関数が自動的にジェネリックになる場合がある。
let identity (x: 'a): 'a = x
let swap (a, b) = (b, a) // 'a * 'b -> 'b * 'a
type Tree<'a> =
| Leaf
| Node of 'a * Tree<'a> * Tree<'a>when 'T : comparison などで型制約を付けられる。比較・等値・演算子の存在を要求できる。
let maximum (xs: 'a list when 'a : comparison) =
List.reduce max xs
let inline addAny (a: ^T) (b: ^T) : ^T =
a + b // + 演算子を持つ型ならすべて OK^T と inline を使うことで、コンパイル時に型を解決し演算子を要求できる(C++ テンプレートに近い)。ゼロコスト抽象。
let inline sumList (lst: ^T list) : ^T =
List.fold (+) LanguagePrimitives.GenericZero lst
sumList [1; 2; 3] // 6(int)
sumList [1.0; 2.0; 3.0] // 6.0(float)コンピュテーション式
応用コンピュテーション式はモナドを読みやすい構文で書くための仕組み。async { ... }・seq { ... }・task { ... } などが代表例。
// seq CE
let evens = seq {
for i in 0..100 do
if i % 2 = 0 then yield i
}
// option CE(カスタム例)
let result = option {
let! x = Some 10
let! y = Some 20
return x + y
}F# の async { } は非同期ワークフロー。let! で非同期値をアンラップ、do! で副作用を実行。Async.RunSynchronously または |> Async.Start で実行。
let fetchAsync url = async {
use client = new System.Net.Http.HttpClient()
let! response = client.GetStringAsync(url) |> Async.AwaitTask
return response.Length
}
fetchAsync "https://example.com"
|> Async.RunSynchronously
|> printfn "Length: %d"F# 6 以降は task { } で .NET の Task を直接扱える。C# の async/await と完全互換。
open System.Threading.Tasks
let fetchTask url : Task<int> = task {
use client = new System.Net.Http.HttpClient()
let! content = client.GetStringAsync(url)
return content.Length
}
fetchTask "https://example.com" |> _.Result |> printfn "%d"型プロバイダー
応用型プロバイダーは外部データソース(JSON・CSV・SQL・XML)から型をコンパイル時に自動生成する F# 固有の機能。型安全なデータアクセスを実現。
// FSharp.Data の JSON 型プロバイダー
#r "nuget: FSharp.Data"
open FSharp.Data
type Weather = JsonProvider<"https://api.example.com/weather">
let data = Weather.Load("https://api.example.com/weather")
printfn "Temp: %.1f" data.Temperature // 型安全にアクセスCSV ファイルのスキーマを自動推論してコンパイル時に型を生成。列名がプロパティとして型安全に使える。
type Stocks = CsvProvider<"stocks.csv">
let stocks = Stocks.Load("stocks.csv")
for row in stocks.Rows do
printfn "%s: %.2f" row.Symbol row.Price
// Symbol, Price は CSV ヘッダーから自動生成された型データベースのスキーマからコンパイル時に型を生成。クエリの型安全性がコンパイル時に保証される。
// SQLProvider 例(概念)
type Sql = SqlDataProvider<"Data Source=mydb.sqlite">
let ctx = Sql.GetDataContext()
query {
for user in ctx.Users do
where (user.Age > 18)
select user.Name
}.NET 連携
基礎F# は .NET の全ライブラリを利用できる。open System で名前空間を開き、System.IO.File などを直接使用可能。
open System
open System.IO
let files = Directory.GetFiles(".", "*.fs")
let content = File.ReadAllText(files[0])
printfn "%s" (DateTime.Now.ToString("yyyy-MM-dd"))F# の query { } コンピュテーション式で SQL ライクな LINQ クエリを書ける。IEnumerable・IQueryable の両方に対応。
let numbers = [1..20]
let result =
query {
for n in numbers do
where (n % 2 = 0)
select (n * n)
take 5
}
// [4; 16; 36; 64; 100].fsproj ファイルで NuGet パッケージを管理。dotnet add package でパッケージを追加。paket は代替の依存管理ツール。
# CLI でパッケージ追加
dotnet add package Newtonsoft.Json
dotnet add package FSharp.Data
# .fsproj に追記される
# <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />テスト
基礎.NET 標準のテストフレームワーク。[・[ アトリビュートでテストを定義。dotnet test で実行。
open Xunit
[<Fact>]
let ``add returns correct sum`` () =
let result = 1 + 1
Assert.Equal(2, result)
[<Theory>]
[<InlineData(1, 2, 3)>]
[<InlineData(0, 0, 0)>]
let ``add is commutative`` a b expected =
Assert.Equal(expected, a + b)F# 向けの軽量テストフレームワーク。関数型スタイルのテスト定義。パフォーマンステストや BDD スタイルもサポート。
open Expecto
let tests = testList "Math" [
test "addition" {
Expect.equal (1 + 1) 2 "should be 2"
}
test "float" {
Expect.floatClose Accuracy.high 3.14 3.14 "should be equal"
}
]
[<EntryPoint>]
let main args = runTestsWithCLIArgs [] args testsFsCheck は Haskell の QuickCheck の F# 版。ランダムな入力を自動生成して性質(プロパティ)を検証する。
open FsCheck
let revRevIsOriginal (xs: int list) =
List.rev (List.rev xs) = xs
Check.Quick revRevIsOriginal
// Ok, passed 100 tests.ビルドツール・エコシステム
基礎dotnet CLI で F# プロジェクトを管理。dotnet new console -lang F# で新規プロジェクト。
dotnet new console -lang F# -o MyApp
dotnet build
dotnet run
dotnet test
dotnet publish -c ReleaseF# は.fsproj でソースファイルの順序が重要(上から順にコンパイル)。前方参照ができないため、依存される型は先に宣言する。
<!-- .fsproj の例 -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="Types.fs" />
<Compile Include="Logic.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
</Project>VS Code の Ionide 拡張が F# の主要 IDE。dotnet fsi で F# Interactive(REPL)を起動できる。スクリプトファイル(.fsx)もサポート。
# FSI 起動
dotnet fsi
# スクリプト実行(.fsx ファイル)
dotnet fsi my_script.fsx
# nuget パッケージを FSI で使う
#r "nuget: FSharp.Data"
open FSharp.Data関数型パターン・設計
応用Result 型を bind(>>=)でチェーンするエラーハンドリングパターン。成功パスと失敗パスを「レール」に例えた設計手法。
let (>>=) result f =
match result with
| Ok v -> f v
| Error e -> Error e
let validateAge age =
if age >= 0 then Ok age else Error "Negative age"
let validateName name =
if name <> "" then Ok name else Error "Empty name"
let result =
Ok { Name = ""; Age = -1 }
>>= (fun p -> validateName p.Name |> Result.map (fun n -> { p with Name = n }))
>>= (fun p -> validateAge p.Age |> Result.map (fun a -> { p with Age = a }))判別共用体とレコードで Value Object・Entity・集約を表現できる。型システムで不正な状態を表現不可能にする「make illegal states unrepresentable」パターン。
// 不正状態を型で排除
type Email = private Email of string
module Email =
let create s =
if s.Contains("@") then Ok (Email s)
else Error "Invalid email"
let value (Email s) = s
// OrderStatus が型で保証される
type Order =
| Pending of items: string list
| Shipped of trackingNo: string
| Delivered(| Name |) でカスタムパターンを定義。既存の型に対して読みやすい分解ルールを追加できる。
let (|Even|Odd|) n = if n % 2 = 0 then Even else Odd
let (|Positive|Negative|Zero|) n =
if n > 0 then Positive
elif n < 0 then Negative
else Zero
match 42 with
| Even & Positive -> "even positive"
| Odd -> "odd"
| _ -> "other"パフォーマンス・Native AOT
応用.NET の Span はスタック割り当てのスライスビュー。ReadOnlySpan で文字列のゼロコピー処理が可能。
open System
let countChars (s: ReadOnlySpan<char>) (target: char) =
let mutable count = 0
for c in s do
if c = target then count <- count + 1
count
let text: ReadOnlySpan<char> = "hello world".AsSpan()
countChars text 'l' // 3.NET 8 の Native AOT で F# コードをネイティブバイナリに事前コンパイル。起動時間とメモリ使用量を大幅削減できる(リフレクション制限あり)。
<!-- .fsproj -->
<PublishAot>true</PublishAot>
# ビルド
dotnet publish -r linux-x64 -c Release
# 自己完結シングルバイナリが生成されるBenchmarkDotNet は .NET の標準ベンチマークライブラリ。[ アトリビューターで計測対象を指定。JIT ウォームアップを考慮した正確な計測ができる。
open BenchmarkDotNet.Attributes
open BenchmarkDotNet.Running
[<MemoryDiagnoser>]
type MyBench() =
[<Benchmark>]
member _.ListMap() = [1..1000] |> List.map ((*) 2)
[<Benchmark>]
member _.ArrayMap() = [|1..1000|] |> Array.map ((*) 2)
BenchmarkRunner.Run<MyBench>() |> ignore