F#はMicrosoftのDon Syme氏が開発したプログラミング言語で、命令型、オブジェクト指向、関数型の要素を併せ持つマルチパラダイム言語と位置付けられています。実装はオープンソースのMITライセンスのもと、githubのリポジトリで公開されています。
実行環境は.NET Frameworkおよび.NET CoreやMonoといったマルチプラットフォームに対応していますが、サポート状況については各プラットフォームごとの情報を参照してください(「F# の概要」など)。
なお、ここで対象とするF#のバージョンは4.1(FSharp.Core 4.4.1.0)とします。
タプルってまぎらわしい
タプルは型が異なる複数の値を1つの組として同時に扱える型のことで(値, 値, ...)
と表します。ただ、このタプルがいろいろとまぎらわしいかったりします。
2つのタプル型
F# 4.1では.NET Framework 4.5 / .NET Standard 1.0から提供されてきたSystem.Tuple(クラスのタプル)と.NET Framework 4.7 / .NET Standard 2.0から新たに提供されるSystem.ValueTuple(構造体のタプル)を扱えます。
System.Tuple
は(1, "uno", obj())
のように表し、型名はint * string * obj
のように型名を*
で区切って示します。System.ValueTuple
はstruct (1, "uno", obj())
のように表し、型名はstruct (int * string * obj)
のように全体をstruct (...)
で囲んで示します。
(1, "uno") = System.Tuple.Create(1, "uno")
struct (1, "uno") = System.ValueTuple.Create(1, "uno")
両者はSystem.TupleExtensionsクラスのメソッドを使って相互に変換できます。
// TupleからValueTupleへの変換
System.TupleExtensions.ToValueTuple((1, "uno")) = struct (1, "uno")
// ValueTupleからTupleへの変換
System.TupleExtensions.ToTuple(struct(1, "uno")) = (1, "uno")
リストや配列の要素をカンマで区切ると...
F#では、リストや配列などのコレクションオブジェクトでは、要素を;
(セミコロン)で区切ります。しかし、誤ってこれらを','で区切るとコレクションの中に1つのタプルだけが存在しているとみなされます。エラーにはなりませんので、まぎらわしく思うときがあるかもしれません。
(* 本来のリスト *)
[1; 2; 3] // int list
[1, 2, 3] // (int * int * int) list [(1, 2, 3)]と同じ
[|1; 2; 3|] // int []
[|1, 2, 3|] // (int * int * int) [] [|(1, 2, 3)|]と同じ
型名が複雑になると...
タプルの型名は複数の型名を*
で区切ったものですが、タプルがネストしたときやoption
, list
などが組み合わされたときはカッコがつくかどうかで階層関係が変わります。データ構造を誤解しないように注意が必要です。
(1, "uno", (2, "dos")) // int * string * (int * string)
(1, (2, "dos"), "uno") // int * (int * string) * string
((1, "uno"), 2, "dos") // (int * string) * int * string
(1, [2; 3]) // int * int list
[(1, 2); (3, 4)] // (int * int) list
(1, Some "uno") // int * string option
Some (1, "uno") // (int * string) option
(1, ["uno"; "dos"]) // int * string list
[(1, "uno"); (2, "dos")] // (int * string) list
(1, Some ["uno"; "dos"]) // int * string list option
(1, [Some "uno"; Some "dos"]) // int * string option list
Some [(1, "uno"); (2, "dos")] // (int * string) list option
[Some (1, "uno"); Some (2, "dos")] // (int * string) option list
Some (1, ["uno"; "dos"]) // (int * string list) option
[(1, Some "uno"); (2, Some "dos")] // (int * string option) list
引数は個別かタプルか
関数の引数が複数あるとき、それらは個別のものか、それともタプルでくくるのかが、関数の型が変わります。
let f x y = x + y // int -> int -> int
let g (x, y) = x + y // int * int -> int
f 1 2 = 3
g (1, 2) = 3
let f x y = x + y
と定義される関数f
の型はint -> int -> int
です。そしてlet g (x, y) = x + y
と定義される関数g
はint * int -> int
です。両者の結果は等価ですが、f
の引数はint
が2つ、g
の引数はint * int
1つで、関数の型は全く異なっています。
そのため、f
は以下のf 1
のように引数が部分適用された関数を定義できますが、g
に対してはこのような関数を定義できません。
let f1 = f 1
f1 2 // 3
let g1 = g 1 // 実行できない(引数の型がgの定義と異なるため)
コンストラクタとメソッドの引数
この違いはコンストラクタやメソッドの型でも見られます。
引数の数が異なるコンストラクタを持つクラスを定義し、それぞれのインスタンスを生成するとき、引数が1つのものは両端に()
がなくても実行できますが、2つのものは両端に()
をつけないと実行できません。これはコンストラクタ実行時の引数列が1つのタプルとして扱われているからです。メソッドでもこれと同じ扱いになります。.NETのクラスなどでも同様です。
(* コンストラクタの定義 *)
type C =
new () = { ... } // () -> Cインスタンス
new (a1) = { ... } // obj -> Cインスタンス
new (a1, a2) = { ... } // obj * obj -> Cインスタンス
(* インスタンス生成 *)
C() // 引数なしを表すカッコ(unit)が必要
C 1 // カッコなしでも実行できる
C(1, 2) // 引数列は1つのタプル - C 1 2 では実行できない
type C() =
member this.Mf x y = x + y
member this.Mg (x, y) = x + y
C().Mf 1 2 // 3
C().Mg (1, 2) // 3
let s = ".NET Framework"
s.replace ("Framework", "Core") // ".NET Core"
s.replace "Framework" "Core" // 実行不可
戻り値がタプルのときがある
.NETのクラスやメソッドを利用するとき、C#でのシグネチャと、F#での型の定義とが異なっている場合があります。
System.Int32
のTryParseメソッドは、C#では戻り値がbool
で、かつ第2引数のout
によって変換の結果を得られるのですが、F#では戻り値の型がbool * int
になっています。つまり、タプルの左の要素は変換の可否を表し、それがtrue
であれば右の要素に変換結果が得られていることを表します。
(* System.Int32.TryParseメソッドの場合 *)
C#: public static bool TryParse (string s, out int result)
F#: TryParse: string -> bool * int
(* F#でInt32.TryParseを実行 *)
let atoi s =
match System.Int32.TryParse s with
| (true , x) -> x
| (false, _) -> sprintf "「%s」を数値に変換できませんでした" s
|> invalidArg "Int32.TryParse"
atoi "123" // 123
atoi "abc" // "System.ArgumentException: 「abc」を数値に変換できませんでした"
要素が1つだけのタプル
タプルは要素を複数持つと紹介しましたが、実は1つしか要素を持たないタプルもつくれないわけではありません。ただ、つくれたとしても一見するとタプルではない値のようにも見えます。
こうしたタプルの型名はSystem.Tuple<'T>
やSystem.ValueTuple<'T>
となります。
let t1 = System.Tuple.Create("uno") // ("uno")
let s1 = System.ValueTuple.Create("uno") // struct ("uno")
(* 型情報のチェック *)
t1.GetType() = typeof<System.Tuple<string>> // true
s1.GetType() = typeof<System.ValueTuple<string>> // true
Item1, Item2, ...にアクセスできない
2つのタプル型はいずれも.NET環境で定義されたクラスと構造体で、個々のタプルはそれらのインスタンスで表されます。両者には、各要素の値を個別に得られるItem1
, Item2
, ...というプロパティ(あるいはフィールド)が提供されます。ですが、F#ではこれらにアクセスできません(対話型環境fsiにて確認)。
(1, "uno").Item1 // アクセスできない
(struct (1, "uno")).Item1 // アクセスできない
ただ、これらのプロパティを使わなくても、それぞれの要素を個別に代入したければ、以下のようにできます。
let (a, b, c) = (1, "uno", obj()) // a = 1; b = "uno"; c = obj()
let struct (a, b, c) = struct (1, "uno", obj()) // 同上
(* mutableな変数にも代入可 *)
let mutable (a, b, c) = (1, "uno", obj())
let mutable struct (a, b, c) = struct (1, "uno", obj())
また、要素が2つのタプルに対しては、左の要素にアクセスするfst
、右の要素にアクセスするsnd
という関数が提供されます。これらは型情報もそのまま残ります。ただしTuple
にしか対応しません。
fst (1, "uno") = 1
snd (1, "uno") = "uno"
ValueTuple
に対応する関数はすぐにつくれます。
let fsts struct (a, _) = a // fsts struct (1, "uno") = 1
let snds struct (_, b) = b // snds struct (1, "uno") = "uno"
要素が少なければこれで良いのですが、要素の数が多くなると、これでは対応できなくなるでしょう。fst
, snd
に相当する関数をタプルの要素の数だけ定義するのも面倒です。
そんなときはF#のリフレクション関数FSharpValue.GetTupleFieldで指定した位置の要素を得られます。この値はobj(System.Object)
型になっていますので、適宜unbox<'T>
を実行してください。この関数はどちらのタプル型にも対応します。
open Microsoft.FSharp.Reflection
let t = (1, "uno")
let s = struct (1, "uno")
FSharpValue.GetTupleField(t, 0) |> unbox<int> // 1
FSharpValue.GetTupleField(s, 1) |> unbox<string> // "uno"
もちろん、Item1, Item2, ...
などにアクセスする方法がないわけではありません。.NETのリフレクションAPIを使えば良いのです。こちらも適宜unbox<'T>
を実行して元の型に戻してください。
(* =は等価の判定 *)
let t = (1, "uno")
t.GetType().GetProperty("Item1").GetValue(t) |> unbox<int> // 1
let s = struct (1, "uno")
s.GetType().GetField("Item2").GetValue(s) |> unbox<string> // "uno"
[おまけ1] 配列とタプルの変換
タプルとobj[]
型(obj
型の配列)との間で変換を行う関数があります。関数はいずれもMicrosoft.FSharp.Reflectionモジュールで提供されます。
現状、タプルからobj[]
型への変換はTuple
とValueTuple
のどちらも対応していますが、その逆はTuple
のみの対応です。ValueType
のタプルを得るには、さらに変換が必要です。もしかすると今後のバージョンアップで対応するかもしれません。
open Microsoft.FSharp.Reflection
(* タプルからobj[]への変換 *)
FSharpValue.GetTupleFields (1, "uno") // [|box 1; box "uno"|]
FSharpValue.GetTupleFields struct(1, "uno") // [|box 1; box "uno"|]
(* obj[]からタプルへの変換 *)
FSharpValue.MakeTuple([|1; "uno"|], typeof<int * string>)
|> unbox<int * string> // (1, "uno")
FSharpValue.MakeTuple([|1; "uno"|], typeof<int * string>)
|> unbox<int * string>
|> System.TupleExtensions.ToValueTuple // struct (1, "uno")
[おまけ2] リストとタプルの変換
必要性はともかくとして、リストとタプルの間で変換する関数もつくってみました。これらもMicrosoft.FSharp.Reflectionモジュールの関数でつくっています。
open Microsoft.FSharp.Reflection
(* リストからタプルに変換する関数 *)
let listToTuple (list: 'T list) =
// listからobj[]に変換
let arr = list
|> List.map (fun item -> box item)
|> List.toArray
// 変換するタプルのType
let typ = list
|> List.length
|> Array.replicate
<| typeof<'T>
|> FSharpType.MakeTupleType
// obj[]からTupleに変換
FSharpValue.MakeTuple (arr, typ)
listToTuple [1; 2] // (1, 2)
open Microsoft.FSharp.Reflection
let tupleToList<'T> (t: obj): 'T list =
t
|> FSharpValue.GetTupleFields
|> Array.map (fun item -> item |> unbox<'T>)
|> Array.toList
tupleToList<int> (1, 2) // [1; 2]
ここをご覧いただいた方々の参考になればと思います。