F# と C# の記法比較

MSDN F# リファレンスを使い C# と記法を比較する

null 値

null 値 Null Values

元ネタ http://msdn.microsoft.com/ja-jp/library/dd233197.aspx

.NET のメソッドに null 値を渡すには、呼び出し元のコードで null キーワードを使用します。

F#

open System

// Pass a null value to a .NET method.
let ParseDateTime (str: string) =
    let (success, res) = DateTime.TryParse(str, null, System.Globalization.DateTimeStyles.AssumeUniversal)
    if success then
        Some(res)
    else
        None

TryParse メソッドは、パースを行って成功/失敗を bool 値を返すと同時に、成功した場合は out で値を返す。 C# の場合は、ParseDateTime メソッドで戻り値を DateTime? にして、失敗の場合は null を返すようにする

C#

public DateTime? ParseDateTime(string str)
{
    DateTime res ;
    if (DateTime.TryParse(str, null, System.Globalization.DateTimeStyles.AssumeUniversal, out res))
        return res;
    else
        return null;
}

.NET のメソッドから取得した null 値を解釈するには、可能であればパターン マッチを使用します。 入力ストリームの末尾を超えて読み取ろうとしたときに ReadLine から返される null 値を、パターン マッチを使用して解釈する方法を次のコード例に示します。

F#

// Open a file and create a stream reader.
let fileStream1 =
    try
        System.IO.File.OpenRead("TextFile1.txt")
    with 
        | :? System.IO.FileNotFoundException -> printfn "Error: TextFile1.txt not found."; exit(1)

let streamReader = new System.IO.StreamReader(fileStream1)

// ProcessNextLine returns false when there is no more input;
// it returns true when there is more input.
let ProcessNextLine nextLine =
    match nextLine with
    | null -> false
    | inputString ->
        match ParseDateTime inputString with
        | Some(date) -> printfn "%s" (date.ToLocalTime().ToString())
        | None -> printfn "Failed to parse the input."
        true

// A null value returned from .NET method ReadLine when there is
// no more input.
while ProcessNextLine (streamReader.ReadLine()) do ()

F# の場合は一時的な関数をその場で書けるが、C# の場合はメソッド化しないといけない。

C#

public void test()
{
    System.IO.FileStream fileStream1 = null;
    try {
        fileStream1 = System.IO.File.OpenRead("TextFile1.txt");
    } catch ( System.IO.FileNotFoundException ex ) {
        Console.WriteLine("Error: TextFile1.txt not found.");
        return ;
    }
    var streamReader = new System.IO.StreamReader(fileStream1);
    // A null value returned from .NET method ReadLine when there is
    // no more input.
    while (ProcessNextLine(streamReader.ReadLine())) ;
}
bool ProcessNextLine(string nextLine)
{
    switch (nextLine)
    {
        case null: 
            return false;
        default:
            var date = ParseDateTime(nextLine);
            if (date != null)
            {
                Console.WriteLine("{0}", date.Value.ToLocalTime().ToString());
            }
            else
            {
                Console.WriteLine("Failed to parse the input.");
            }
            return true;
    }
}

ただし、必ずメソッド化しないと駄目というわけでもなく、ラムダ式を使うと似た感じで書ける。

C#

public void test2()
{
    System.IO.FileStream fileStream1 = null;
    try
    {
        fileStream1 = System.IO.File.OpenRead("TextFile1.txt");
    }
    catch (System.IO.FileNotFoundException ex)
    {
        Console.WriteLine("Error: TextFile1.txt not found.");
        return;
    }
    var streamReader = new System.IO.StreamReader(fileStream1);

    Func<string, bool> ProcessNextLine = nextLine =>
    {
        switch (nextLine)
        {
            case null:
                return false;
            default:
                var date = ParseDateTime(nextLine);
                if (date != null)
                {
                    Console.WriteLine("{0}", date.Value.ToLocalTime().ToString());
                }
                else
                {
                    Console.WriteLine("Failed to parse the input.");
                }
                return true;
        }
    };
    // A null value returned from .NET method ReadLine when there is
    // no more input.
    while (ProcessNextLine(streamReader.ReadLine())) ;
}

F# の型に対する null 値は、Unchecked.defaultof を呼び出す Array.zeroCreate を使用する場合など、別の方法でも生成されることがあります。

F#

match box value with
| null -> printf "The value is null."
| _ -> printf "The value is not null."

box を使う必要は C# ではないと思われる。

値 Values

元ネタ http://msdn.microsoft.com/ja-jp/library/dd233185.aspx

値の束縛

束縛とは、名前と定義を関連付けることを意味します。 let キーワードは、次の例のように、値のバインディングを行います。

F#

let a = 1
let b = 100u
let str = "text"

// A function value binding.

let f x = x + 1

正確には const をつけて変更できないようにするのだが、コード的には変更しなければ良い。

C#

const int a = 1;
const char b = (char)100;
const string str = "text";
// A function value binding.
Func<int, int> f = x => x + 1;

変更不可である理由

純粋な関数型言語では、変数は存在せず、関数は数学関数として厳密に機能します。 手順型言語のコードでは変数割り当てを使用して値を変更しますが、関数型言語の等価なコードには、入力される変更不可の値、変更不可の関数、および出力される別の変更不可の値があります。 この数学的な厳密性により、プログラムの動作についての強力な推論が実現します。 この強力な推論により、コンパイラでコードをより厳密にチェックし、より効率的に最適化を行い、開発者が正しいコードを容易に理解および記述できるようになります。 したがって、関数型コードは、通常の手順型コードよりもデバッグが容易になるのが普通です。

要は、変更しないことを優先することによって、変更しているかもしれないリスク(変数の汚染)を減してデバッグを容易にする、ってこと。また、型推論を優先させるためには、変更しないことを優先するほうがよい。

変更可能な理由

変更可能な変数に初期値を割り当てるには、値を定義するときと同じ方法で、let キーワードを使用します。 ただし、次の例に示すように、<- 演算子を使用して、変更可能な変数の後に新しい値を割り当てることができます。

F#

let mutable x = 1
x <- x + 1

C# の場合は、変数>値になるので、数の変更は楽。

C#

int x = 1 ;
x = x + 1 ;

インライン関数

インライン関数 Inline Functions

元ネタ http://msdn.microsoft.com/ja-jp/library/dd548047.aspx

inline 修飾子は、最上位の関数、モジュール レベルの関数、またはクラスのメソッド レベルの関数に適用できます。
最上位のインライン関数、インラインのインスタンス メソッド、およびインラインの静的メソッドを次のコード例に示します。

F#

let inline increment x = x + 1
type WrapInt32() =
    member inline this.incrementByOne(x) = x + 1
    static member inline Increment(x) = x + 1

C# にはインラインがない。JIT で最適化するからだそうだ。

Why doesn't C# have an 'inline' keyword? - Eric Gunnerson's Compendium - Site Home - MSDN Blogs http://blogs.msdn.com/b/ericgu/archive/2004/01/29/64644.aspx

外部関数

外部関数 External Functions

元ネタ http://msdn.microsoft.com/ja-jp/library/dd393785.aspx

次のエクスポート関数を含むネイティブ C++ DLL があると仮定します。

#include <stdio.h>
extern "C" void __declspec(dllexport) HelloWorld()
{
    printf("Hello world, invoked by F#!\n");
}

この関数は、次のコードを使用して F# から呼び出すことができます。

F#

open System.Runtime.InteropServices

module InteropWithNative =
    [<DllImport(@"C:\bin\nativedll", CallingConvention = CallingConvention.Cdecl)>]
    extern void HelloWorld()

InteropWithNative.HelloWorld()

書き方は C# でも F# でも同じ。

C#

using System.Runtime.InteropServices;

class InteropWithNative
{
    [DllImport(@"C:\bin\nativedll", CallingConvention = CallingConvention.Cdecl)]
    public static extern void HelloWorld();
}

class Program()
{
    void Main()
    {
        InteropWithNative.HelloWorld();
    }
}

エントリ ポイント

エントリ ポイント Entry Point

元ネタ http://msdn.microsoft.com/ja-jp/library/dd402151.aspx

簡単な main 関数の例を次に示します。

[<EntryPoint>]
let main args =
    printfn "Arguments passed to function : %A" args
    // Return 0. This indicates success.
    0

C# の場合は、こんな感じ。Main 関数が呼び出される。

C#

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine( "Arguments passed to function : {0}", args );
    }

再帰関数: rec キーワード

再帰関数: rec キーワード Recursive Functions: The rec Keyword

元ネタ http://msdn.microsoft.com/ja-jp/library/dd233232.aspx

rec キーワードは、再帰関数を定義するときに let キーワードと一緒に使用します。

n 番目のフィボナッチ数を計算する再帰関数を次のコードに示します。

F#

let rec fib n =
   if n <= 2 then 1
   else fib (n - 1) + fib (n - 2)

「メソッドは型の中では暗黙的に再帰的になります。rec キーワードを追加する必要はありません。 クラス内の Let 束縛は、暗黙的に再帰的ではありません。」ということなので、let する時だけ rec が必要になる。

C# の場合は、そのまま。

C#

public int fib(int n)
{
    if (n <= 2)
        return 1;
    else
        return fib(n - 1) + fib(n - 2);
}

相互再帰関数

このような関数は、and を使用して 1 つの let 束縛で一緒に定義し、相互にリンクする必要があります。

F#

let rec Even x =
   if x = 0 then true
   else Odd (x - 1)
and Odd x =
   if x = 1 then true
   else Even (x - 1)

C# の場合は、そのまま。

F#

public bool Even(int x)
{
    if (x == 0)
        return true;
    else 
        return Odd(x - 1);
}
public bool Odd(int x)
{
    if (x == 1)
        return true;
    else
        return Even(x - 1);
}

ラムダ式: fun キーワード

ラムダ式: fun キーワード Lambda Expressions: The fun Keyword

元ネタ http://msdn.microsoft.com/ja-jp/library/dd233201.aspx

ラムダ式、つまり匿名関数を定義するには、fun キーワードを使用します。

F#

// Lambda expressions with parameter lists.
fun a b c -> ...
fun (a: int) b c -> ...
fun (a : int) (b : string) (c:float) -> ...

// A lambda expression with a tuple pattern.
fun (a, b) ->// A lambda expression with a list pattern.
fun head :: tail ->

C# でも書き方は似ている。fun キーワードはいらない。

C#

// parameter lists and tuple pattern
Func<int, string, float, int> f = (a, b, c) => 0;
// list pattern はこんな感じ?
Func<int, List<int>, int> f2 = (head, tail) => head + tail.Sum();

list pattern の場合は、head と tail を取り出せば良いので、こんな感じか?

expression は関数の本体であり、その最後の式によって戻り値が生成されます。 有効なラムダ式の例は次のとおりです。

F#

fun x -> x + 1
fun a b c -> printfn "%A %A %A" a b c
fun (a: int) (b: int) (c: int) -> a + b * c
fun x y -> let swap (a, b) = (b, a) in swap (x, y)

最後の swap は無理矢理 F# と同じように書くとこんな風になる。

C#

Func<int, int> f = x => x + 1;
Func<object, object, object, object> f2 =
    (a, b, c) =>
    {
        Console.WriteLine("{0} {1} {2}", a, b, c); return null;
    };
Func<int, int, int, int> f3 = (a, b, c) => a + b * c;
Func<object, object, object> f4 = (x, y) =>
{
    Func<Tuple<object, object>, Tuple<object, object>> swap =
        (t) =>
        {
            return new Tuple<object, object>(t.Item2, t.Item1);
        };
    return swap(new Tuple<object, object>(x, y));
};

ただし、内部で swap 関数を作る必要がなければ、Tuple を返すだけでよい。

C#

Func<object,object,Tuple<object,object>> f5 = 
    (a,b) => new Tuple<object,object>( b, a);

ラムダ式の使用

この場合は、匿名関数が、リストのすべての要素に 1 を追加します。

F#

let list = List.map (fun i -> i + 1) [1;2;3]
printfn "%A" list

C# には map 関数がないので自作するのだが、次のように拡張メソッドで作れる。ただし、拡張メソッドでは T が使えないので int 型限定。C# 6.0 ではジェネリックが使える予定なハズ。

C#

void test3()
{
    var lst = new List<int> { 1, 2, 3 };
    var dest = lst.Map(x => x + 1);
}
public static class ListExtensions
{
    public static List<int> Map( this List<int> src, Func<int,int> fn) {
        var dest = new List<int>();
        foreach (var it in src)
        {
            dest.Add(fn(it));
        }
        return dest;
    }
}

適用関数とリストの順序を守って、ListEx という新しいクラスを作ると T で書ける。

C#

void test4()
{
    var lst = new List<int> { 1, 2, 3 };
    var dest = ListEx<int>.Map(x => x + 1, lst);
}
public class ListEx<T>
{
    public static List<T> Map(Func<T, T> fn, List<T> src)
    {
        var dest = new List<T>();
        foreach (T it in src)
        {
            dest.Add(fn(it));
        }
        return dest;
    }
}