最近少し F# を触ってみています。
F# では let inline
で関数定義をすることでインライン関数を定義することができるようになっており、この中では SRTP (Statically Resolved Type Parameter) と呼ばれる特殊な型変数が使えるようになっています。
詳しくは こちらの記事 を参照してみてください。
SRTP を用いることで、通常は認められないような型クラスに相当する型制約が実現できます。
ちょっと具体例を見てみましょう。
// 通常の関数定義では以下の関数は多相にはならず、オーバーロードされた (+) のデフォルト実装 int -> int -> int が使われる.
> let add x y = x + y ;;
val add : x:int -> y:int -> int
// SRTP を用いることで、(+) の実装が存在するあらゆる型に対して適用可能な関数となる.
> let inline add (x : ^T when ^T : (static member (+) : ^T -> ^T -> ^T))
(y : ^T) =
x + y ;;
val inline add :
x: ^T -> y: ^T -> ^T when ^T : (static member ( + ) : ^T * ^T -> ^T)
// SRTP は明示的に書かずとも型推論によって導出できる.
> let inline add x y = x + y ;;
val inline add :
x: ^a -> y: ^b -> ^c
when ( ^a or ^b) : (static member ( + ) : ^a * ^b -> ^c)
通常の多相関数の型変数は 'a
や 'T
と表記されますが、SRTP は ^a
や ^T
のように表記されます。
let inline
で定義した add
関数は (+)
の実装が存在する型ならばどのような型に対しても適用することができます。
> add 1 2 ;;
val it : int = 3
> add 1L 2L ;;
val it : int64 = 3L
> add 1.0 2.0 ;;
val it : float = 3.0
> add 1.0f 2.0f ;;
val it : float32 = 3.0f
便利ですね。
では、この SRTP を利用して任意の数値型に対して 10
を加える関数を書いてみたいとしましょう。
以下のように書けそうですが...
> let inline add10 x = x + 10 ;;
val inline add10 : x:int -> int
しかしこのように書いてしまうと int -> int
と推論されてしまいます。これは 10
という数値リテラルが int
型であると推論されるためです。
Haskell では数値リテラルは多相定数として扱われますが、F# では OCaml と同様に数値リテラルは多相ではないため、このように数値リテラルをそのまま使用してしまうと SRTP を使っても多相が実現できないという問題が生じます。
解決策
ここ や ここ でも紹介されていますが、NumericLiteralG
のように NumericLiteral
の後ろに Q
、R
、Z
、I
、N
、G
のいずれかのアルファベットを付与した名前のモジュールを定義することで多相数値リテラルを独自に定義するという方法が使われるようです。
これは F# の仕様書 では 6.3.1 Simple Constant Expressions で説明されており、Q
、R
、Z
、I
、N
、G
のいずれかの文字を数値リテラルの suffix として使用すると、その suffix に対応するモジュールの関数呼び出しへと syntactic translation が行われる という性質を利用したものです。
例えば suffix として G
を使用する場合、100G
のようにして通常の数値リテラルの後ろにこの suffix を記述すると、これが NumericLiteralG.FromInt32(100)
という関数呼び出しへと変換されることとなります。
以下に F# の仕様書に記載されている変換ルールを転載します。
xxxx<suffix>
For xxxx = 0 → NumericLiteral<suffix>.FromZero()
For xxxx = 1 → NumericLiteral<suffix>.FromOne()
For xxxx in the Int32 range → NumericLiteral<suffix>.FromInt32(xxxx)
For xxxx in the Int64 range → NumericLiteral<suffix>.FromInt64(xxxx)
For other numbers → NumericLiteral<suffix>.FromString("xxxx")
したがって、以下のようにして FromZero
、FromOne
、FromInt32
といった関数を let inline
で定義したモジュールを用意することで、SRTP と一緒に使える多相数値リテラルを実現することができます。
module NumericLiteralG =
let inline FromZero() = LanguagePrimitives.GenericZero
let inline FromOne() = LanguagePrimitives.GenericOne
let inline FromInt32 (n:int) =
let one : ^a = FromOne()
let zero : ^a = FromZero()
let n_incr = if n > 0 then 1 else -1
let g_incr = if n > 0 then one else (zero - one)
let rec loop i g =
if i = n then g
else loop (i + n_incr) (g + g_incr)
loop 0 zero
やっていることは基本的に LanguagePrimitives.GenericZero : ^a
および LanguagePrimitives.GenericOne : ^a
の加算を繰り返すことで任意の整数値を計算するという単純なものです。
FSharpPlus を使う
このようなモジュールを用意すれば多相的な数値リテラルを扱うことはできるのですが、いちいち自分でこんなモジュールを用意するのは面倒ですね。
FSharpPlus
ライブラリには既にこのようなモジュールが用意されているので、このライブラリを使えばすぐに多相数値リテラルが使えます。
FSharpPlus
は NuGet で配布されており、.NET Core では dotnet
コマンドから簡単にインストールできます。
$ dotnet add package FSharpPlus
インストールが問題なく行われれば、.fsproj
ファイルに以下の内容が追記されるはずです。
<ItemGroup>
<PackageReference Include="FSharpPlus" Version="1.0.0" />
</ItemGroup>
FSharpPlus
のソースコード を見てみると、FSharpPlus.Math.Generic
モジュールの中で NumericLiteralG
モジュールが定義されています。
したがって FSharpPlus.Math.Generic
モジュールを open
することによって 100G
などの多相数値リテラルが使用できるようになります。
(100G
のようなリテラルが NumericLiteralG.FromInt32(100)
のように読み替えられることになるので、NumericLiteralG
モジュールが見えるように FSharpPlus.Math.Generic
モジュールを開いておく必要があります。)
早速試してみましょう。dotnet fsi
で F# Interactive を起動します。
> #r "<ユーザのホームディレクトリ>/.nuget/packages/fsharpplus/1.0.0/lib/netstandard2.0/FSharpPlus.dll" ;;
> open FSharpPlus.Math.Generic ;;
> let inline add10 x = x + 10G ;;
val inline add10 :
x: ^a -> ^a
when (FSharpPlus.Control.FromInt32 or ^a) : (static member FromInt32 : ^a *
FSharpPlus.Control.FromInt32
->
int32 ->
^a) and
(FSharpPlus.Control.Plus or ^a) : (static member ( + ) : ^a * ^a *
FSharpPlus.Control.Plus
-> ^a)
> add10 5 ;;
val it : int = 15
> add10 5L ;;
val it : int64 = 15L
> add10 5. ;;
val it : float = 15.0
> add10 5.f ;;
val it : float32 = 15.0f
> add10 5M ;;
val it : decimal = 15M
このように、簡単に多相数値リテラルが使えるようになります。
注意点としては、この多相数値リテラルは 整数としてしか書けない という点が挙げられます。
したがって、10G
とは書けても 10.0G
や 10.G
とは書けません。
これはちょっと不便ですが、通常の用途の範疇ならば多相数値リテラル同士の分数として表現すればよいのでそこまで大きな問題にはならないかと思います。
> let pi: float = 3141592654G / 1000000000G ;;
val pi : float = 3.141592654
> let pi: float32 = 3141592654G / 1000000000G ;;
val pi : float32 = 3.141592503f
> let pi: decimal = 3141592654G / 1000000000G ;;
val pi : decimal = 3.141592654M
> let pi: int = 3141592654G / 1000000000G ;;
val pi : int = -1
> let pi: int64 = 3141592654G / 1000000000G ;;
val pi : int64 = 3L
(上の例では int
型の値がおかしなことになっていますが、これは 3141592654
が 32 ビットの範囲では表現できない整数であるためです。)
> let i = int32 3121592564L ;;
val i : int32 = -1173374732
> let j = int32 1000000000L ;;
val j : int32 = 1000000000
> i / j ;;
val it : int32 = -1
FSharpPlus
では Functor
や Monad
も提供されており、多相数値リテラルも使うことで Haskell のようなコードを書くことができます。
> open FSharpPlus ;;
// Functor
> map ((+) 10G) (Some 5) ;;
val it : int option = Some 15
> map ((+) 10G) (Some 5.) ;;
val it : float option = Some 15.0
> map ((+) 10G) [ 1L ; 2L ; 3L ] ;;
val it : int64 list = [11L; 12L; 13L]
> map ((+) 10G) [ 1.f ; 2.f ; 3.f ] ;;
val it : float32 list = [11.0f; 12.0f; 13.0f]
// Applicative
> (+) <!> Some 1. <*> Some 2G ;;
val it : float option = Some 3.0
> (+) <!> [| 1L ; 2L ; 3L |] <*> [| 10G ; 100G ; 1000G |] ;;
val it : int64 [] = [|11L; 101L; 1001L; 12L; 102L; 1002L; 13L; 103L; 1003L|]
> (+) <!> seq { 1I ; 2I } <*> seq { 10G } ;;
val it : seq<System.Numerics.BigInteger> =
seq [11 {IsEven = false;
IsOne = false;
IsPowerOfTwo = false;
IsZero = false;
Sign = 1;}; 12 {IsEven = true;
IsOne = false;
IsPowerOfTwo = false;
IsZero = false;
Sign = 1;}]
// Monad
> Some 1L >>= (fun x -> Some (x + 100G)) ;;
val it : int64 option = Some 101L
> monad {
- let! x = Some 10.f
- let! y = Some 100G
- return (x + y)
- } ;;
val it : float32 option = Some 110.0f
> monad {
- let! x = [ 1M ; 2M ; 3M ]
- let! y = [ 10G ; 100G ; 1000G ]
- return (x + y)
- } ;;
val it : decimal list = [11M; 101M; 1001M; 12M; 102M; 1002M; 13M; 103M; 1003M]
References
- https://qiita.com/cannorin/items/59d79cc9a3b64c761cd4
- https://stackoverflow.com/questions/13016257/using-fs-static-type-parameters-and-encoding-numeric-constants
- https://stackoverflow.com/questions/4732672/how-to-write-a-function-for-generic-numbers
- https://fsharp.org/specs/language-spec/4.1/FSharpSpec-4.1-latest.pdf
- https://github.com/fsprojects/FSharpPlus/blob/master/src/FSharpPlus/Operators.fs#L879
- http://tomasp.net/blog/fsharp-generic-numeric.aspx/
- https://fsprojects.github.io/FSharpPlus/abstraction-functor.html
- https://fsprojects.github.io/FSharpPlus/abstraction-applicative.html
- https://fsprojects.github.io/FSharpPlus/abstraction-monad.html