🎸

C++のcinをF#で実装する

2020/10/26に公開

C++のcinから標準入力を受け取って>>の演算子を使って変数へと代入するというのをF#で実装する、というものです。
ネタ的な要素と、演算子のオーバーロードを利用してこんな風なことができるという紹介が主となります。

#include <iostream>

int main() {
    int a,b;
    std::cin >> a >> b;
    std::cout << a << b;
}
入力
123 456
出力
123456

上記のようなものをF#で記述できるようにします。

実装

module IOStream =
    type CIn () =
        // 省略
        member __.Read () : 'T =
            match typeof<'T> with
            | ty when ty.Equals(typeof<int64>)  -> __.Long() |> box
            | ty when ty.Equals(typeof<int32>)  -> __.Long() |> int32  |> box
            | ty when ty.Equals(typeof<bigint>) -> __.Long() |> bigint |> box
            | ty when ty.Equals(typeof<float>)  -> __.Next() |> float  |> box
            | ty when ty.Equals(typeof<double>) -> __.Next() |> float  |> box
            | ty when ty.Equals(typeof<string>) -> __.Next() |> box
            | _ -> __.Next() |> box
            :?> 'T // downcast

    type COut () =
        do new StreamWriter(Console.OpenStandardOutput(), AutoFlush=false) |> Console.SetOut
        let sb = StringBuilder()

        member __.Write(str : obj) =
            str |> string |> sb.Append |> ignore
        
        interface IDisposable with
            member __.Dispose() =
                sb.ToString() |> Console.Write
                Console.Out.Flush()
                     
module Main =  
    open System
    open System.Text
    open System.Collections.Generic
    open System.ComponentModel
    open IOStream
    
    let inline (>>) (cin : CIn) (x : outref<'U>) =
        x <- cin.Read()
        cin

    let inline (<<) (cout : COut) (x : obj)  =
        cout.Write(x)
        cout

    let endl = "\n"

    [<EntryPoint>]
    let main argv =
        let cin = new CIn()
        use cout = new COut()
        let mutable a,b,c,s = 0,0,0,""
        // #nowarn "0020" を記述しない場合、警告が出ます。
        cin >> &a >> &b >> &c >> &s
        cout << (a+b+c) << " " << s << endl
        0
出力
> 123 456 100000 hello,world!
100579 hello,world!

IOStreamモジュールはC#で競プロやっている方のソースでよく見る、高速な標準入出力のライブラリをF#に書き換えたものです。
今回、cinっぽい書き方をするために書き換えていますが、長いので省略しています。

outref<'T>

いわゆる参照渡しですが少し注意が必要で、C#のoutパラメータ修飾子がF#でのbyref<'T>に相当するもので、refoutrefininrefとなります。
C#でのout int xはF#ではx : byref<int>となります。引数として渡す変数はmutableである必要があります。また、変数名の頭に&を記述する必要があります。

ちなみに、F#ではoutパラメータ修飾子のついた引数に代入される値を受け取る時に、タプルを利用してこのように書けたりします。

let returns, result = Int32.TryParse("123");;
// val returns : bool = true
// val result : int = 123

演算子のオーバーロード

既存の演算子をオーバーロードする他にも、! % & * + - . / < = > ? @ ^ | および ~ を使って演算子を自作することもできます。
>>, <<は元々関数の合成の機能を持つ演算子で、以下のように使用します。

let f x = // 'T1 -> 'T2
    // 'T1の型を持つ値を引数として受け取り'T2の型を持つ値を返す

let g x = // 'T2 -> 'T3
    // 同上

let h = // 'T1 -> 'T3
    f >> g

これを左辺にCIn型の値を、右辺にoutref<'T>型の値を取り、CIn型の値を返す演算子にオーバーロードしました。
ちなみに、演算子を関数として使用することもできます。

let main argv =
    let cin = new CIn()
    let mutable a = 0
    (>>) cin &a
    0

終わりに

関数型言語の文脈としてはあまり良くないように思いますが、それらしく書けたのではないでしょうか。
FSharp.Coreの実装を今一度よく読んで>>演算子の実装や、入力の受取・変換をもう少しスマートに書きたいところです。

参考

C#でC++のcinっぽいものを作ってみる - Qiita

Discussion