これは、 F# Advent Calendar 2021 の15日目の記事です
なぜ F# なのか
皆さん Rust 書いてますか?良い言語ですね。私も好きです。私は C や C++ をちゃんと扱ったことがなかったので、Rust を学ぶことで、ムーブセマンティクスや左辺値と右辺値、スタックとヒープの使い分け、static dispatch と dynamic dispatch やスレッド安全性を徹底する方法など多くのことを学びました。
ゼロコスト抽象化を目標に掲げているだけあり、広い領域に適用できる言語である一方で、業務上は Rust が適さない場面も多くあります。一言でいうと、「借用チェッカがオーバーキルになる場合」です。
例えば
- 探索的なプログラミング
- データサイエンス
- GUI
などです。私の場合、コーディングを通してドメイン知識を理解することが業務の殆どを占めるので、借用チェッカに煩わされるよりも、 GC のオーバーヘッドを許容してコーディングを楽にすることの方が大事です。
したがって、 Rust を業務で使うことはあまりありませんでした。
そんな中、 Rust for Fsharpers and F# for Rustaceans というリポジトリを見つけました。
以下、冒頭部分の要約です
Rust は私の最初の言語であった。そして最近、 Rust の周辺言語に手を出している。
そのうちの一つが F# である。 (Rust Compiler はもともと OCaml で書かれており、文法も似通っているためである。)
F# の manual はほぼ全て C# 開発者向けに書かれているが、私にとっては Rust との共通点の方が多く、 Rust で説明してくれたほうがわかりやすい
私も全く同様のことを感じており、 F# を「軽く扱えるRust」のような捉え方をしていたので、同じように F# を扱う人が増えてくれることを願ってこの記事を書いています。
Rust Playground か、 Try F# でそれぞれの言語をインストール無しで試せるので、興味がある方はいじってみてください
Rust と F# の比較
ここからは文法を比較していきます。
いずれの言語も式 (Expression) ベースである
いずれも、関数や scope の返り値は最後に評価した式となるので return
文をほとんど使いません。
Rust は {}
で scope を区切りますが、 F# は (python のように) indent が scope となります。したがって、以下の2つは同等です。
Rust
fn main() {
let z = {
let x = 5;
let y = 10;
x + y
};
}
F#
let main() =
let z =
let x = 5
let y = 10
x + y
F# では ;
と {}
が必要なく、かつ借用チェッカも存在しないため、このようにしょっちゅうスコープを区切ります。以下のようにベタ書きにするよりも x
と y
が一時変数であることが明らかなので、よりリーダブルなコードになります。
// あまり良くない例
let x = 5
let y = 10
let z = x + y
// ...ここから先で x, y を使うことができてしまう。
Primitive 型の比較
Rust と F# のいずれも、コンパイラが型推論してくれます。
primitive 型の種類もだいたい同じです。以下に、名前の比較を載せます。
rust | F# |
---|---|
u8 | uint8(byte, System.Byte) |
u16 | uint16 |
u32 | uint32 |
u64 | uint64 |
i8 | int8(System.SByte) |
usize | unativeint(System.UIntPtr) |
size | nativeint |
f32 | float32 (single, System.Single) |
f64 | float (double, System.Double) |
また、数字リテラルに接尾子をつけることで、型を指定できる点も同様です。
Rust
let x: u8 = 5
let x = 5u8
F#
let x: uint8 = 5uy
let x = 5uy
一覧にするとこんな感じです。
rust | F# |
---|---|
5u16 | 5us |
5u32 | 5u |
5u64 | 5UL |
5i8 | 5y |
5i16 | 5s |
5i32 | 5 |
5i64 | 5L |
5.0f32 | 5.0f., 5f |
5.0f64 | 5.0, 5. |
また、 F# の decimal 型は Rust には標準で存在しませんが、 5m
という書き方をします。
数字の間に _ を入れて読みやすくすることができるのも同様です。
let num = 8_000_000;
その他の型について ...
- F# の
char
は Rust のchar
に似ていますが、 前者が UTF-16 なのに対し、 Rust の char は UTF-32 で常に 4bytes です。これは、 Rust とは違い、 char の配列がほぼそのまま string として扱えることを示しています。 - Rust の
unit
と F# のunit
は両方とも()
で表され、役割も同じです。
文字列型に関しては Rust には色々あるので、簡単な比較はできませんが、 string
は immutable な char の配列で、 mutable に扱いたい場合は StringBuilder
を使う。と覚えておけば良いです。
Rust の &str
に一番近い型は ReadOnlySpan<char>
です。同様に、 &mut str
は Span<char>
で扱います。
異なる型へのキャスト
Rust は、 as
か、 std::mem::transmute
を使います。
Rust
// 例えば、 isize を引数に取る関数の場合
fn print_i64(number: i64) {
println!("{}", number);
}
fn main() {
let x: i8 = 9;
print_i64(x as i64)
}
F# の場合、primitive 型ならば型と同名の関数を使います。
つまり、上の Rust code は以下と等しい ということです。
let print_i64 (number: i64) =
printfn $"%i{number}"
let x: int8 = 9y
print_i64(int64 x)
なお、 F# は引数や返り値の型も推論してくれるので、指定する必要はありません。
let printNumber number =
printfn $"%i{number}"
また、 primitive 型以外に関しては :>
でアップキャスト、 :?>
でダウンキャストを行う事もできます。(ダウンキャストは実行時例外を投げる可能性があるので注意)
inline
キーワードの扱い
F# の inline
キーワードは、 Rust の inline 関数とは少し扱いが異なり、 static dispatch 、すなわちジェネリックな関数を定義するために利用します。
上記の printNumber
関数は、引数の number の型を特定の数値型にコンパイル時に解決しますが、その特定の型は一つのみです。つまり、以下はコンパイルに通りません。
let printNumber(foo) =
printfn $"%i{foo}"
let x:int8 = 1y
foo(x)
// 違う型で再度呼び出し。
let y: int = 1
foo(y) // コンパイルエラー
そこで、 printNumber
を inline
にすることで、 自動的に static dispatch してくれます。
let inline printNumber foo =
printfn $"%i{foo}"
引数の Trait Boundary に相当するものを自動で推論してくれるので楽です。
コンパイル時型チェックという静的型言語の利点を損なうことなく、スクリプト言語のように簡単にかけるので、探索的データ分析などに重宝します。 (こういった理由から MS は F# をデータサイエンス向け言語として押し出し始めています)
match 式
使用感はだいたい同じなので、簡単な比較にとどめます。
以下の2つは同等です。
Rust
enum Mood {
Good,
Okay,
Bad
}
fn check_mood(mood: Mood) {
match mood {
Mood::Good => println!(“Feeling Good”),
Mood::Bad => println!(“Not so good”),
_ => println!("I don't know how I feel")
}
}
F#
type Mood =
| Good
| Okay
| Bad
let checkMood myMood =
match myMood with
| Good -> printfn “Feeling good”
| Bad -> printfn “Not so good”
| _ -> printfn "I don't know how I feel"
わかりやすいですね。なお、 F# では function
キーワードでも同様のことができます。
let checkMood =
function
| Good -> printfn “Feeling good”
| Mood.Bad -> printfn “Not so good” // `Bad` と `Mood.Bad` は同等
| _ -> printfn "I don't know how I feel"
パターンマッチ対象は Enum に限らない点も同様ですが、 F# のほうが適用範囲が広く、 Interface にもマッチさせられます。interface は trait みたいなものです。(厳密には違うけど)
カリー化
rust と違い、 F# では関数を返す関数を簡単に定義することができます。
複数の引数を取る関数を定義する際、例えば Rust では
fn add_three(a: i32, b: i32, c: i32) -> i32 {
a + b + c
}
となるところ、 F# では以下のように書きますが
let addThree(a, b, c) = a + b + c
以下のように書くこともできます。
let addThree a b c = a + b + c
こうすることで、変数を一つづつ適用することができます。
let add9AndAddTwo = addThree 9
let add7AndAddTwo = addThree 7
同等のものを Rust で書こうとすると、以下のようになり非常に面倒です
fn add_three (a: i32) -> Box<dyn Fn(i32) -> Box<dyn Fn(i32) -> i32>> {
Box::new(move |b: i32| { Box::new(move |c: i32| { a + b + c }) })
}
(Rust を「関数型言語」と呼ぶことに対する私の違和感は、このように関数を気軽に扱えない点から来ています。)
カリー化することにどんな利点があるのかというと、例えば map, filter, fold, reduce のような汎用処理をより気軽に行えることが挙げられます。
以下の2つは同等ですが、 F# のほうが簡潔です。
Rust
(1..10)
.fold(0, |a, b| a + b)
F#
[1..10]
|> List.fold (+) 0 // あるいは List.reduce (+) でもよい。
この簡潔さは、パイプライン処理と組み合わせた際に更に顕著になります。
パイプライン処理
F# は、 Rust よりも自由にカスタム演算子を定義することができます。
デフォルトで利用できる演算子の中に、 |>
や >>
, ||>
と言ったパイプライン演算子があります。
例えば以下のように、値を左から右に流していく処理をかけます
let addOne x = x + 1
let timesTwo x = x * 2
let printIt x = printfn "%A" x
// 左から右に処理していくため、読みやすい
8 |> addOne |> timesTwo |> printIt
// 以下も同等
// `>>` は、関数を結合させる演算子
let execAll =
addOne >> timesTwo >> printIt
8 |> execAll
// 処理内容は同等だが、読みづらい
printIt(timesTwo(addOne 8))
なお、
%A
は{:?}
(デバッグプリント) に相当するものです。
Rust でも、 メソッドチェーンで読みやすくすることはできますが、 ちょっと面倒です。
let times_two_then_even: Vec<i32> = (0..=10)
.map(|number| number * 2)
.filter(|number| number % 2 == 0)
.collect();
println!("{:?}", times_two_then_even);
これを F# で書くと
let timesTwoThenEven =
[0..10]
|> List.map (fun number -> number * 2)
|> List.filter (fun number -> number % 2 = 0)
printfn "%A" timesTwoThenEven
となります。 fun
は、 Rust の ||{}
に相当します。つまりクロージャ構文です。
更に縮めて以下のようにもできます。
let timesTwoThenEven =
[1..10]
|> List.map ((*)2)
|> List.filter((%)2 >> (=)0)
型定義
Rust の Struct と F# のレコード class/struct
いずれの言語も、代数学的データ構造をあらゆる処理に使います。
Rust
// 直和型
enum Mood {
Good,
Okay,
Bad
}
// 直積型
struct Person {
first_name: String,
last_name: String,
mood: Mood,
}
F#
/// 直和型, Union と呼ぶ
type Mood =
| Good | Okay | Bad
/// 直積型, Record と呼ぶ
type Person = {
FirstName: string
LastName: string
Mood: Mood
}
あ、そういえば
///
で doc comment がかける点も Rust と同じです
これらの型はほぼ同じように扱えますが、重要な違いもあります。
F# では Debug
, Display
, Hash
, Eq
, ParitalEq
, Ord
, PartialOrd
は #derive
しなくても自動実装してくれます。(ただし、下位の型がそれらを実装していない場合は除く)
Copy
, Clone
, Default
は、 struct の場合のみ自動実装してくれます。 (後述)
tuple struct や、 NewType に相当するものは、 Single case union (右辺が一つしか無い直和型)として書くことができます。
つまり、以下の2つは同等です。
struct Foo {}
struct MyNewType(Foo)
struct MyTupleStruct(u8, u8, u8);
/// マーカー型
type Foo = Foo
/// Struct attribute をつけることで、より Rust の struct に近くなる
[<Struct>]
type MyNewType = MyNewType of Foo // rust と同様、実行時オーバーヘッドはかからない
/// 右辺(case 名)は左辺(型名)と同一である必要はない。
[<Struct>]
type MyTupleStruct = ThisCanBeWhateverYouWantInSingleCaseUnion of uint8 * uint8 * uint8
ヒープとスタック
通常、 F# の型は基本的には primitive 型を除きヒープに作成されます。
対して、 struct は stack 上に作られるような型です。(勝手に box 化されたりすることもあるので、 rust の struct と完全に同一ではないです。)
[<Struct>]
という Attribute をつけることで、任意の型を stack に置くことができます。
/// stack 上の直和型
[<Struct>]
type Mood =
| Good | Okay | Bad
/// stack 上の直積型
/// なぜか type のあとに記述しても良い。
type [<Struct>] Person = {
FirstName: string
LastName: string
Mood: Mood
}
Attribute は、 F# では
[<>]
、 Rust では#[]
で書きますが、扱いは2つの言語でだいたい一緒です。
struct は、 Rust における #derive[Clone, Copy, Derive]
した型と同様に振る舞いますが、 F# でも &
演算子や、 ref<_>
などのポインタ型を使うことで、 参照渡しすることができます。
また、 F# には無名 Record というのもあります。型定義が面倒な場合に便利です
let person = {| FirstName = "John"; LastName = "Doe" |}
演算子のオーバーローディング
Rust では、 +
や *
のような演算子はコンパイラが特別扱いしており、対応するトレイト (Add
, Deref
など) を実装することで独自の型に適用することができます。
対して、 F# では、演算子はただの関数です。適当な記号を ()
で囲んだ値を関数名にすることで自動的に演算子となります。
ですので、独自演算子も結構気軽に定義します。以下ではついでにメソッドの構文も解説しています。
type Cost = {
Fee: int64
Tax: int64
}
// メンバ関数。 Rust で言うところの `fn (&self)` メソッド
with
member this.GetTotal() = this.Fee + this.Tax
// プロパティ。 引数のないメンバ関数
member this.Total = this.GetTotal()
// static メソッド
static member Zero = { Fee = 0L; Tax = 0L }
// static メソッドとして演算子を定義
static member (+) (a, b) = {
Fee = a.Fee + b.Fee
Tax = a.Tax + b.Tax
}
// 以下のように、普通の関数として定義してもよい。
let (+) (a: Cost) (b: Cost) = {
Fee = a.Fee + b.Fee
Tax = a.Tax + b.Tax
}
let costA = {
Fee = 10L
Tax = 10L
}
let costB = {
costA with Tax = 0L
}
// 演算子の利用
let totalCost = costA + costB
// Rust と同様、普通のメソッドとして使うこともできる
let totalCost = Cost.(+)(costA, costB)
ですので、コンパイラが想定していないものでも自由に演算子として定義することができます。
この機能を活用している例に、パーサコンビネータなどがあります。
ジェネリクス
いずれも <>
をジェネリクスのために使いますが、若干の差異があります。
<’a>
のように、’
がついた場合、 Rust ではライフタイムパラメータであることを明示していますが、 F# の場合はジェネリクスの場合、そもそも必ず ’
を付ける必要があります。つまり、 <a>
のような指定の仕方は、F# の場合は a
という型が実際に存在する場合を除いてありえないということです。
trait boundary に相当する事もできますが、 F# のほうがより柔軟です。 trait に限らず、特定のメソッドを持つことを条件にすることもできます。
(面倒になってきたのでコードは割愛)
オブジェクトの更新とミュータビリティ
いずれも、パフォーマンス上の理由などが無い限り、データをイミュータブルなものとして扱います。例えば struct の update はそれぞれ以下のように書きます
Rust
let person = Person {
first_name: "foo".to_string(),
last_name: "bar".to_string(),
mood: Mood::Good,
};
let grumpy_person = Person {
mood: Mood::Bad,
..person
};
F#
let person = {
FirstName: "foo"
LastName: "bar"
Mood: Mood.Good
}
let grumpyPerson = {
person
with Mood = Mood.Bad
}
ミュータブルな変数の扱いも両者で似ています。
以下の2つのコードブロックは同じ内容です。
Rust
let mut x = 7;
x = 8
F#
let mutable x = 7
x <- 8
F# は、Rust よりも mutability を嫌う傾向が強いため、 mutable
と長めのキーワードを使い、更に破壊的代入に独自の演算子 <-
を用いています。
ミュータビリティが必要になる場面もだいたい同じです。
code の順番
Rust では、単一のスコープでのコードの記述順は重要ですが、スコープ外ではそうではありません。
例えば、以下の2つのはいずれも問題なくコンパイルします。
struct Child {}
struct Parent {
child: Child
}
struct Parent {
child: Child
}
struct Child {}
しかし、 F# で同等のものを書くと、2つ目のコードはコンパイルしません。
F# では依存される型は、常に依存する型より先に宣言されている必要があります。
これは、単一のファイル内に限らず、 .fs
ファイル間にも適用されます。つまり F# では、ファイルに上下関係があ
るということです。
これははじめは窮屈ですが、実際には F# の機能の中でもかなり嬉しい特徴です。
型間の依存関係が明確になり、コードを理解するのがずっと簡単になるのです。
モジュール
Rust の mod
に相当するものは、 module
キーワードです。以下が対応するコードブロックです。
mod my_private_module {
// ...
}
pub(crate) mod my_internal_module{
// ...
}
pub mod my_public_module {
// ...
}
module private MyPrivateModule =
// ...
module internal MyInternalModule =
// ...
module MyPublicModule =
// ...
コレクション型
list
Rust で多用されるコレクション型といえば、 Vec<T>
ですが、 同等の型は F# では ResizeArray
と呼ばれます。 的確な名前ですね。
配列 Array
、 []
という名前です。これも全く同じです。
唯一異なるのが、 F# の list
型です。これは単方向連結リストです。
Rust でも、近い型は存在しますが、利用は推奨されていないのに対し、 F# ではしょっちゅう使います。
なぜ推奨されていないかと言うと、パフォーマンス上の理由です。 パフォーマンスに気を使わないなら Rust を使う意味がないので当然と言えるでしょう。
ただ、 immutable に扱えるという利点もあるので、 関数型言語ではよく使います。
例えば、 list を再帰関数で処理する例が以下です。
// rec をつけることで再帰関数となり、末尾最適化を行ってくれる
let rec doubleIfEven l =
match l with
// 空じゃない list にマッチ
| h::tail ->
/// 先頭の要素に適当な処理をおこない。
let newHead = if h % 2 = 0 then h * 2 else h
// 先頭を除外した残りの要素に再帰的に同じ処理を適用していったものを返す。
newHead::doubleIfEven(tail)
// 最後の要素の場合は空リストをそのまま返す。
| [] -> []
以下も同様です。
let rec doubleIfEven =
function
| h::tail ->
(if h % 2 = 0 then h * 2 else h)::doubleIfEven(tail)
| _ -> []
[]
でリストを、 seq []
で イテレータを、 [||]
で配列をそれぞれ宣言することができます。
それらのすべてで、 python のリスト内包表記に相当することを行うことができます(後述)
HashMap
に相当する型としては、イミュータブルな Map
があります。
コレクション型以外のモナド
コンピュテーション式 (computation expression)
F# 特有の機能として、コンピュテーション式というものがあります。
これは、 python の list 内包表記と monad binding 構文 (Haskell の do
, Scala の for
など)を合体させたようなものです。
後者の monad binding は、 Rustacean にとっては、 ?
や async/await
として馴染みが深いと思います。 コンピューテーション式では、同様のことを Option
や Seq
(Iterator
) などの任意の monad に行うことができます。
monad とは、(実用上は)
bind
メソッドを持っている型です。 (rust では、and_then
という名前です)
以下で、それぞれの monad について構文を比較していきます。
Option
/Result
Option
と Result
は、いずれの言語でも扱い方は同じです。 mut 参照の扱いや、変数のムーブを考えなくて良い分 F# のほうが楽です。
Rust
fm main(){
let _ =
Some(1)
.filter(|x| x % 2 == 0)
.map(|x| Some(x * 2))
.flatten()
.and_then(|x| Some(x + 1))
.unwrap_or(100);
let x = Some(1);
if let Some(v) = x {
println!("{:?}", v);
}
}
同等の F# コード
let _ =
Some(1)
|> Option.filter((%)2 >> (=)0)
|> Option.map((*)2 >> Some)
|> Option.flatten
|> Option.bind((+)1 >> Some)
|> Option.defaultValue(100)
let x = Some(1)
// if let には、 `iter` 関数が近い
x |> Option.iter(fun v -> printfn $"%A{v}")
以下は Result
の比較です。
Rust
let r = Ok(1);
let _ =
r.clone()
.map(|x| x * 2)
.and_then(|x| Ok(x + 1))
.unwrap_or(100);
let _ =
r
.or_else(|_: String| Err("Failed".to_string()))
.unwrap_err();
F#
open FsToolkit.ErrorHandling
let r = Ok(1)
let _ =
r
|> Result.map((*)2)
|> Result.bind((+)1 >> Ok)
|> Result.defaultValue 100
let _ =
r
|> Result.mapError(fun _ -> "Failed")
|> Result.either
(fun _ -> failwith "panic") // Ok ならば panic する
id // Error ならばそのまま返す (fun x -> x と同じ)
F# は標準ライブラリに Result をハンドリングする関数群がついていないので、 FsToolkit.ErrorHandling を使うか、自分で定義するのが定番です。
F# のほうが型推論が強力なので、わからないところは適当に obj
で埋めてくれたりします。
(あと、好みの問題ですが、 Rust の Result
関連メソッドって名前が分かりづらい気がします...)
私にとっては、 F# は Rust よりもエラーハンドリングがしやすいです。
なぜかというと、いずれの言語も例外を投げる(panic
する)可能性は常にあるのですが、 F# ではtry-catch
で気軽にハンドリングできるためです。
Rust ではUnwindSafe
を考慮に入れる必要があるので難易度が上がります。
monad binding は以下のようになります
Rust
fn result_binding() -> Result<i32, String> {
let r = Ok::<i32, String>(1);
let v = r?;
assert_eq!(1, v);
Err("Returning Error".to_string())
}
F#
open FsToolkit.ErrorHandling
let resultBinding() = result {
let r = Ok(1)
let! v = r
assert(1 = v)
return! Error("Returning Error")
}
?
operator に相当するものは、 let!
文になります。 result{}
というのがコンピュテーション式で、この内部では Result
型を let!
などの特殊文法で扱いますよという意味です。
Rust では、 Result
を特別扱いしてますが、 コンピュテーション式では、任意の型を扱えます。例えば、 option の場合、以下のように書くこともできます
let optionBinding() = option {
let o = Some(1)
let! v = o
assert(1 = v)
return! None
}
これには以下のようなメリットがあります。
- 自作の型に対して処理を定義できること
- 未知の型でも同じように扱え、理解が用意になること
- Applicative binding など、更に柔軟な処理も統一的に行えること
応用例として、 query 式や、 seq
, 非同期処理などがあります。
非常に柔軟なので、 rust における macro のように、言語内 DSL のような役目を果たすことができます。
また、新しい型でもすぐに同じように扱えるという利点があります。
例えば Option
を struct にした ValueOption
というのが最近導入されましたが、全く同じように扱えます。
Future/Async/Task
非同期処理は、 Async
または Task
で行います。
元々 F# では非同期処理は、 Async
で行っていたのですが、その後 Task
と async/await
という形で C# に移植され、逆輸入されたので2種類あります。
現在は、 Task
を使うことのほうが多いです。コンピュテーション式のおかげでほぼ同じように扱えます。
Rust の Future
と同じで、実態はスレッドをまたがって実行されるクロージャです。
以下比較です
Rust
async fn future_handling() -> i32 {
let x = future::ready(1).await;
if x == 1 {
return x
}
0
}
F#
open FSharp.Control.Tasks
let taskHandling = task {
let! x = Task.FromResult(10)
return
if x = 1 then x else 0
}
// こんな書き方もできる
let taskHandling2 = task {
match! Task.FromResult(10) with
| x when x = 1 ->
return x
| _ ->
return 0
}
// `Async` 型もほぼ同じ
let asyncHandling = async {
let! x = Async.singleton 10
return
if x = 1 then x else 0
}
適当な runtime に spawn したり、 Pinning を考慮したりと行った面倒が無い点は Rust より楽ですが、歴史的経緯からいろんな型を扱わなければならなかったりするので、簡単さはどっこいどっこいです。
Seq
コンピュテーション式では、 for, while や try-catch などの挙動も定義することができます。
これにより、 seq
(Iterator
に相当) や、 asyncSeq
(Stream
に相当)
なども統一的なやり方で扱えるようになり、大変便利です。
例えばイテレータの要素から2つずつの順列と組み合わせを計算してイテレータを返す関数はそれぞれ以下のように簡単にかけます。
let permutation source = seq {
for x in source do
for y in source do
(x, y)
}
let combination source = seq {
for x in source do
for y in source do
if x >= y then (x, y)
}
rust で同等のことを娘なう場合、 Vec に押し込んで返すか
generator を使うか、自前のマクロを定義するかになります。
最後に、もうちょっと複雑な非同期処理の比較をのせます。
(あまり良い例じゃないかも。 そこまで Rust の非同期処理に詳しくないので...)
Rust
use futures::channel::mpsc;
use futures::join;
fn send_recv() {
const BUFFER_SIZE: usize = 10;
let (mut tx, mut rx) = mpsc::channel::<i32>(BUFFER_SIZE);
let write_task = async {
tx.try_send(1).unwrap();
tx.try_send(2).unwrap();
drop(tx);
};
let read_task = async {
let mut i = 0;
while let Some (x) = rx.try_next().unwrap() {
if i == 0 {
assert_eq!(1, x);
}
if i == 1 {
assert_eq!(2, x);
}
i = i + 1;
}
};
join!(write_task, read_task);
}
F#
open FSharp.Control.Tasks
open System.Threading.Channels
// `let [<Literal>]` は `const` に相当
let [<Literal>] BUFFER_SIZE = 10
let sendRecv() =
let tx, rx =
let ch = Channel.CreateBounded<int32>(BUFFER_SIZE)
ch.Writer, ch.Reader
let writeTask = task {
// `unwrap()` に相当する処理を行いたくないので、確実に書き込めるようになるまで待つ
let! _ = tx.WaitToWriteAsync()
do! tx.WriteAsync(1)
let! _ = tx.WaitToWriteAsync()
do! tx.WriteAsync(2)
tx.Complete()
}
let readTask =
async {
for i, x in rx.ReadAllAsync() |> AsyncSeq.ofAsyncEnum |> AsyncSeq.indexed do
if i = 0L then
assert(x = 1)
elif i = 1L then
assert(x = 2)
else
()
}
|> Async.StartAsTask
Task.WhenAll(readTask, writeTask)
F# にあって Rust にない機能
その他、 F# 特有の機能として units of measure や、 型プロバイダがあります。
いずれも便利ですが、長くなってきたので説明は割愛します。
Rust にあって F# にない機能
Rust にあって F# にほしい機能の筆頭としては、マクロが挙げられるでしょう。
これはどうしようもないので、他のメタプログラミング手法で代用するしかありません。
でもどうせみんなマクロなんてちょっとコピペの手間減らすくらいにしか使わないでしょ
あと、 F# でパフォーマンスを追求しようとすると、 クロージャが参照をキャプチャできなかったりしてハマることがあります。
IO bound でない部分でパフォーマンスを気にする可能性が高いなら、最初から Rust で書いたほうが良いです。
その他
GUI
自分が F# を使っているもう一つの大きな理由として、 GUI が書きやすいというものがあります。
自分のようなフロントエンドが本職でないエンジニアが簡単な GUI を作る際、 Bolero が便利です。超便利です。
F# から JS へのトランスパイラは昔からあったのですが、 WASM で完結できるようになったのはものすごい進歩です。
(Bolero は Blazor の薄いラッパーなので、 Blazor がすごいとも言える)
とにかく良いので皆使いましょう。
あ、 デスクトップアプリなら Avalonia.FuncUI というのもあります。これも最高なのでぜひ。
すべて elmish architecture で作ります。 Rust では iced が近いです
web app
言語そのものの機能ではないですが、 ウェブアプリケーションフレームワークも ASP.NET という MS がメンテナンスしてくれているものがあります。
広範囲のライブラリに対して標準が提供されているので、どのライブラリを使うか悩んだり、複数種類を学ぶオーバーヘッドを被る必要がないという利点があります。
終わりに
「C# の進捗が早すぎて F# は過去の言語になったんでしょ?」と言われるのを聞いたことがあるんですが、全然そんなことはないです。
Rust と二刀流にすると、お互いに足りないところを補いつつ、スイッチングコストを小さく抑えることができるで捗るのではないでしょうか。
是非検討してみてください。