8
3

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 5 years have passed since last update.

F#Advent Calendar 2017

Day 20

タプルってまぎらわしい。何がまぎらわしいって...

Posted at

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.ValueTuplestruct (1, "uno", obj())のように表し、型名はstruct (int * string * obj)のように全体をstruct (...)で囲んで示します。

2つのタプル型がある
(1, "uno") = System.Tuple.Create(1, "uno")
struct (1, "uno") = System.ValueTuple.Create(1, "uno")

両者はSystem.TupleExtensionsクラスのメソッドを使って相互に変換できます。

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と定義される関数gint * int -> intです。両者の結果は等価ですが、fの引数はintが2つ、gの引数はint * int1つで、関数の型は全く異なっています。

そのため、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
引数の数と型の違い(.NETのメソッド)
let s = ".NET Framework"
s.replace ("Framework", "Core")  // ".NET Core"
s.replace "Framework" "Core"     // 実行不可

戻り値がタプルのときがある

.NETのクラスやメソッドを利用するとき、C#でのシグネチャと、F#での型の定義とが異なっている場合があります。

System.Int32TryParseメソッドは、C#では戻り値がboolで、かつ第2引数のoutによって変換の結果を得られるのですが、F#では戻り値の型がbool * intになっています。つまり、タプルの左の要素は変換の可否を表し、それがtrueであれば右の要素に変換結果が得られていることを表します。

C#のシグネチャとF#の型が異なる
(* 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>となります。

要素が1つだけのタプル
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,snd関数はTupleにのみ対応
fst (1, "uno") = 1
snd (1, "uno") = "uno"

ValueTupleに対応する関数はすぐにつくれます。

ValueTypeに対応するfst,snd相当の関数
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>を実行してください。この関数はどちらのタプル型にも対応します。

F#のリフレクション関数で指定した位置の要素を得る
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>を実行して元の型に戻してください。

.NETのリフレクションAPIによるプロパティやフィールドへのアクセス
(* =は等価の判定 *)
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[]型への変換はTupleValueTupleのどちらも対応していますが、その逆はTupleのみの対応です。ValueTypeのタプルを得るには、さらに変換が必要です。もしかすると今後のバージョンアップで対応するかもしれません。

タプルとobj[]型との変換
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]

ここをご覧いただいた方々の参考になればと思います。

8
3
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
8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?