8
5

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# の inline 関数中で Haskell のような多相数値リテラルを扱う

Posted at

最近少し 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 の後ろに QRZING のいずれかのアルファベットを付与した名前のモジュールを定義することで多相数値リテラルを独自に定義するという方法が使われるようです。

これは F# の仕様書 では 6.3.1 Simple Constant Expressions で説明されており、QRZING のいずれかの文字を数値リテラルの 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")

したがって、以下のようにして FromZeroFromOneFromInt32 といった関数を 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.0G10.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 では FunctorMonad も提供されており、多相数値リテラルも使うことで 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

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?