NEXTSCAPE blog

株式会社ネクストスケープの社員による会社公式ブログです。ネスケラボでは、社員が日頃どのようなことに興味をもっているのか、仕事を通してどのような面白いことに取り組んでいるのかなど、会社や技術に関する情報をマイペースに紹介しています。

MENU

F# 6でtask式なるものが追加されていたので、F#と併せて紹介してみる

エンタープライズサービス部の橋本です。はてなブログ初投稿です。 この記事は「NEXTSCAPE Advent Calendar 2021」の9日目です。

qiita.com

今年11月の.Net 6のリリースに合わせて、F# 6がリリースされました。

F# 6での変更の中で、これはおそらくF#を始めるにあたっての敷居もすこーし下がったんじゃないかなー、という箇所を見つけたので、F#自体の紹介も兼ねて書かせていただきます。

F#とは?

F#は、Microsoftが出しているプログラミング言語の一つです。

いわゆる関数型(HaskellやScala、OCamlなどが同じ分類)の言語ですが、C#に含まれているライブラリであれば.Netの資産としてあらかた利用することができます。 逆に、F#で書いたライブラリはC#側で参照することも可能です。

C#や.Netを使ったことがあればだいぶ触れやすい言語かと思います。

最初に書いた通り今年11月にF# 6が出たわけですが、その中でタスク式というものが追加されました。 F#のコンピュテーション式というものの追加になるんですが、コンピュテーション式自体についてはこの記事ではあまり触れません。

サンプルとしてコードを以下に書いてみるので、F#についてあまり知らない方も読み物として見ていただいて、雰囲気を感じ取ってもらえたら幸いです。

タスク式

もともとF#には、async式というものがあります。 今回追加されたタスク式と同じコンピュテーション式の一つで、ともにF#の中で非同期処理を書くための構文となっています。 色々かいつまんで言えば、C#でいうasync/await構文のようなものです。

async式を利用したファイルの読み込みを行うライブラリは、例えば下記のように書くことができます。

namespace FileReaderLib

open System.IO
open FSharp.Control.CommonExtensions

module FileReaderWithAsync = 
    let readWithAsync (path: string) = 
        async {
            use sr = new System.IO.StreamReader(path)
            let stream = sr.BaseStream
            let! text = stream.AsyncRead 2
            return System.Text.Encoding.ASCII.GetString(text)
        }

    let readWithAsync2 path = 
        async {
            let! text = File.ReadAllTextAsync(path) |> Async.AwaitTask
            return text
        } |> Async.StartAsTask

readWithAsync、readWithAsync2がそれぞれ実処理にあたります。 ともに、文字列でパスを受け取って、パス先のファイルを読み、中身を返す関数です。

C#のawaitと似たことをしているのがlet! text = ***となっている部分で、***という非同期処理のタスクを剥がして、処理結果をtextという変数に入れています。

上記の関数をライブラリに入れ、C#から読み込もうとすると下記のように書けます。

using FileReaderLib;

var path = Console.ReadLine();
if (path != null)
{
    // var text = await FileReaderWithAsync.readWithAsync(path);
    // Console.WriteLine(text);

    var text2 = await FileReaderWithAsync.readWithAsync2(path);
    Console.WriteLine(text2);
}

C#で作成したライブラリと同様に、usingでライブラリを読み込み、関数として呼び出すことができます。

が。

    // var text = await FileReaderWithAsync.readWithAsync(path);
    // Console.WriteLine(text);

readWithAsyncの方は、返り値の型がMicrosoft.FSharp.Control.FSharpAsync<string>という型になっています。 これがF#内で使用していたasync式の返り値です。

FSharpAsync<T>の型は、F#のasync式内ではそのまま使用することができますが、C#のTask<T>などのクラスとは互換性がないため、当然async/awaitでは利用できません。

一方readWithAsync2はC#のasync/awaitで使えますが、逆にF#内での書き方が煩雑になっています。

    let readWithAsync2 path = 
        async {
            let! text = File.ReadAllTextAsync(path) |> Async.AwaitTask
           // ↑ReadAllTextAsync(path)の結果(Task<T>クラス)を、FSharpAsync<T>に変換している
            return text
        } |> Async.StartAsTask
        // ↑async式全体の返り値(FSharpAsync<T>)を、Task<T>クラスに変換している

上記のように、FSharpAsyncとTaskをたびたび処理内で変換しています。 この変換がコードの見た目としても、実際の処理としてもボトルネックになっていました。

F#6で追加されたタスク式で、同じような処理を書き直したのが下記です。

namespace FileReaderLib

module FileReaderWithTask =
    let readWithTask path = 
        task {
            let! text = File.ReadAllTextAsync(path)
            return text
        }

タスク式の中では、C#側のTask<T>クラスをそのまま利用することができます。 使用感は近いまま、Task<T>を内部で利用できるようになったことで、.Netの非同期処理を扱うライブラリも取り入れやすくなっています。

そして返り値もTask<T>クラスになっているので、C#側でも下記のようにそのままasync/awaitに突っ込めます。

using FileReaderLib;

var path = Console.ReadLine();
if (path != null)
{
    var text = await FileReaderWithTask.readWithTask(path);
    Console.WriteLine(text);
}

F#内だけで処理を書くのであればasync式、FSharpAsync<T>を利用するのがベターになりますが、.Netの資産を使いたい!C#と相互運用したい!という場合にはかなり便利な書き方になっています。

総括

いかがでしょうか。 F#という言語自体あまり触れたことがある人も多くないのではないかと思うのですが、C#の知識を活かしつつ書ける言語なので、(特に関数型言語という名前とかの)敷居の高さは決して思ったほどではありません。

調べてみるといろいろ魅力がある言語なので、ぜひ皆様も触ってみてください!