なぜF#を選んだ?Railsアプリ書き換えでHaskell、Scala、OCamlを蹴ってF#に決めた理由が納得すぎた
引用元:https://news.ycombinator.com/item?id=43546004
F#はRuby on Railsアプリを書き換える時に最高の関数型言語だったんだよねー。HaskellとかOcamlとかScalaも見たけど。Microsoftテクノロジーは詳しくなかったから、F#は一番なさそうだったんだけど、一番の選択肢になったんだよね。Haskellの純粋さはちょっと難しくて、Ocamlのエコシステムはイマイチだったし。Scalaは複雑そうだったし…。F#は意外と簡単に始められたんだよね。コミュニティは корпоратив-mindedな感じだけど、メンバーはフレンドリーで賢くて助けてくれるよ。エコシステムも良くて、dotnetライブラリも使えるし。FsHttpとかFliとかwebsharperとか、良いライブラリやフレームワークもあるしね。マジでF#最高!
ちなみに、F#のslackはもう死んでて、コミュニティはDiscordに移行したみたいだよ。
https://discord.com/invite/fsharp-196693847965696000
ブログにも書いてないけど、Haskellの純粋さが難しかったってどういうこと?Haskellをプロダクションで使ってるけど、純粋さは気にならないな。ほとんどの関数はI/Oコンテキストにあるから、他の言語と変わらないし。
Haskellの純粋さに対するイメージが、始めた頃は難しかったなー。以前命令的に作ったプログラムをHaskellで書き直そうとしたんだけど、それがそもそもピュアなやり方じゃ難しくて。当時はzipersとかknot-tyingとか使って非効率的なことしてたけど、今は普通にやるかな。
言語を選ぶ時って、簡単に手に入る情報とかから、これから起こりうる問題を推測したりするよね。Rescriptのobject typeが嫌で使わないって人もいたけど、Rescriptでobject typeなんて使わないし。IO monadとかも、使ったことないと想像しにくいし、影響がどれくらいあるかとかもわからないし。
>You gotta remember people are often picking languages based on what they can easily find out about it and extrapolating/guessing about what problems they’ll run into with their expected use.”
それマジでそう思う。LispもSyntaxだけで拒否する人多いし。RescriptもReasonとJSの距離を縮めてJSデベロッパーを引きつけようとしたんだよね。
もう6年前くらいだから覚えてないんだけど、Haskellを勉強し始めた時にそう思ったんだよね。今なら違う結論になるかも?わからん。=«とか.とか$とかの特殊な演算子も理解するのが難しかった。F#の方がアプローチしやすくて、関数型のコンセプトを学べたんだよね。
HaskellはIOとparityの違いとか、プログラミングの方法に影響を与えてくれたけど、複雑すぎて使いたくない。
ライブラリはどうなの?特定のmonadを使わされることない?
使ってるライブラリはIOがベースになってるか、少なくともIOを許可してるから、liftIOでIOアクションできるよ。
Ruby on RailsからElixirに移った人が結構いたのに、なんで検討リストに入ってないの? なんか理由ある?
メッセージで言うべきだったんだけど、強い型付けの言語を探してたんだよね。動的型付けの言語を愛用してたんだけど、Ruby on Railsのアプリが保守不能になっちゃって、原因の一つが動的型付けだったんだ。
静的型付けの言語なら、複雑なアプリでも長期的に保守しやすくなるかなと思って。そしたらマジでそうなって、もう動的型付けの言語で開発したくないレベル。
F#を学びながらライブラリ作ったんだけど、dotnetでasyncが主流なのを見逃して全部同期処理で書いちゃって。プロジェクトの半分くらいで気づいてasyncに切り替えたんだけど、動的型付けだったら地獄だったと思う。強い型のおかげで大規模なリファクタリングも自信を持ってできたよ。
アプリはオープンソースでここにあるよ: https://gitlab.com/myowndb/myowndb
細かいことだけど、”強い型付け”と”静的型付け”は同じ意味じゃないよ。ここでは型システムの静的な性質に関心があるんだよね? ”強さ”とは違うものだよ。
マジでその通り。指摘ありがとう。
どういたしまして! じゃあね!
強い型付けと静的型付けについてもっと詳しく教えて!
強い型付け:2 + ”2” はエラーになる(Pythonとか)。
静的型付け:2 + ”2” はコンパイル/パースできない(mypyとかTypescript)。
単純な例だけど、違いを感じてもらえるかな?
>static typing: 2 + ”2” does not compile/parse (e.g. Python vs mypy, Typescript vs JS)“
この例は正しくないと思うな。静的型付けは異なる型の値がどう相互作用するかに影響しないから。2 + ”2”
が有効な式になる静的型付け言語は知らないけど、静的型付け言語は弱く型付けされる可能性があるよ。一番良い例はCで、明示的に型変換しなくても異なる型の値を組み合わせられる(2 + 2.0
)。
強い/弱いと静的/動的は直交してると思う。例を挙げると:
・強い: 2 + ”2”
はエラー
・弱い: 2 + ”2”
は4になる
・静的: var x = 2; x = ”2”
はエラー
・動的: var x = 2; x = ”2”
はOK
動的型付けでも後者(実行時)を禁止できるけど、実装によるね。潜在的型付けってのもあって、これは型が変数じゃなくて値に関連付けられる。
でも動的言語でも変数に型を関連付けて、最初にチェックした後に型が変わるのを禁止できる。
>But a dynamic language can have types associated with variables, and it can forbid changing those types after their types have been checked the first time.
それってC++のauto
みたいな感じ?
weak typingかー。2 + ”2”が22になっちゃうんだね。 ”4”とか4!もありえるね。4が一番たちが悪そうだけど。 一番ヤバいのはC言語かも。2+”22” = 0、4+”4” = 未定義の動作で、たぶん他の変数の値になっちゃう。 >”22”+2 = ””のことじゃない?2番目のポインタを作るのはUBじゃないよ、使うときだけ。 最近はBEAM VM向けの強く型付けされた代替としてGleam[0]があるよ。まだ使ったことないけど、安全+ミニマル+生産性のスイートスポットに一番ハマりそう。(C言語風のsyntaxはちょっとイヤだけど、syntaxは言語の重要な側面じゃないしね。) Gleamは、BEAMをターゲットにしてる言語と同じように、F#に比べて1、2桁遅いよ。 魅力はruntime modelだよね。BEAM言語が本当に遅いのかどうかはすぐには確認できないけど、とりあえずそう仮定しよう。 .NETのSignalRはマジで良いよ。server[0]で強く型付けされたmessage hubが使えるし、client supportも広いし。web socketをscaleさせるためのinfrastructureを持ちたくないならAzure SignalR[1]もあるし。 Apache Spark、Delta LakeはScalaで書かれてるし。JVMベースだから、大きなecosystemもあるし。F#よりScalaの方が良い選択肢かもね。 良い選択肢になりえるのは確かだけど、俺にとってはそうじゃなかった。 .NETで動くんだから、そりゃそうでしょ。小さいプラットフォームじゃないし。 基本的に全部OS上で動くし、そっちの方がもっと複雑じゃん。 F#ってScalaより習得簡単かな? (Scalaは昔の2.x時代にちょっと触ったことあるけど、F#は全然知らない) JVMエコシステムでFPやるならClojureの方がいいと思うなー。Scalaの問題は、関数型とオブジェクト指向の両方をサポートしてるところ。Scalaで関わったコードベースは全部ごちゃ混ぜになっちゃって困るんだよね。でも最高の関数型言語は、もちろんElixir! :D >Every code base I’ve worked on in Scala has ended up being a hodgepodge of both Elixirに強い型システムが入るのは面白いけど、Gleamにも注目だね。ただ、computation expressionsがないのが残念。https://github.com/CaptnCodr/Fli と https://github.com/fsprojects/FsHttp みたいな面白い可能性が開けるのに。 Gleamはlispスタイルのマクロがないし、BEAMとOTPの実装も完全じゃないんだよね。例えば、Gleamはホットアップデートとか、完全な分散システムサポートとか、低レベルなプロセス操作とかをサポートしてない。静的型付けのためにBEAMとOTPのフルパワーを捨てるのはちょっと考えられないなー。 今の俺の好みは、Elixirとその素晴らしいエコシステムをプロジェクトのシェルとして使って、コアなビジネスロジックをGleamで実装すること。 Clojureも同じようにマルチパラダイムじゃないの? もっとそうかも? そうは思わないなー。関数型+JVMへのブリッジって感じかな。言語自体にはそんなに多くのものはないし。関数型プログラミングが基本で、オブジェクトJVMのものをフックするためのツールがあるけど、それは言語の自然な流れじゃない。 F#試したけど、.NETの環境が初めてだったんだよね。hello worldやるだけで、プロジェクトファイルとかボイラープレートがめっちゃ多くてビックリして、やる気なくなったわー。FPとかimmutableとかモダンな言語はマジで賛成なんだけど、仕事ないじゃん?いいコード書いたって評価してくれる会社どこだよ?今はみんなAIで使いやすい言語とか、人員削減とか、生産性アップとかばっか考えてるし。20年プログラミングやってて4~5言語できるけど、インドはもっとひどくて、EUならまだJavaとかTypeScriptで何とか生活できるレベル。Kotlin + TypeScriptですら良い給料の仕事見つけるの難しいのに、ElixirとかClojureとかF#なんて無理ゲー。 relocateすれば70K/yearくらいの仕事あるかもだけど。だからニッチな言語勉強するの諦めたんだよね。 F#のシンタックスは、純粋なロジックだけやってる時はマジで好き。でも、データベースとかREST APIとかのI/O処理が必要になると、MLの美しいシンタックス捨てて、 computation blocksとかいうクソダサいやつ使わないといけないんだよね。C#ならこんな感じ。 個人的には、C#よりCRUDアプリに向いてると思うけどな。どっちも書いたことあるし。シンタックスの比較も、C#のコードに必要になるラッパーとかボイラープレートを考慮してないからフェアじゃない。awaitはどこでも使えるわけじゃないし。async使う時は、どのcontextで実行するか指定する必要があるけど、これはTask/Asyncを現在のsync contextで実行したくない時に便利。最近は、C#みたいな挙動が欲しいならtask使うのが普通だし、custom F# CE’sでSyncContextのオーバーロードを減らすライブラリもある。 F#はでかい言語で、MLのマルチパラダイム言語でC#と相互運用できるから、複雑になるのは仕方ないし、同じことをやる方法もたくさんある。強いメリットは、最初に動く関数型プロトタイプを作って、遅い部分をホットスポット最適化して、イミュータブルな関数をミュータブルな関数で置き換えて高速化できること。PythonとC++みたいに、パフォーマンスが重要な部分だけC++で書き換えるみたいな感じ。C#のawaitは、C#がF#の相互運用を考慮して設計しなかったのが残念。F#はtask builderを追加して、awaitをtask builderブロック内でlet!で置き換えられるようにした。 async CEの特別な挙動が必要な場合を除いて、task CEを常に使うのがベスト。元のコメントの人は、そのことを知らないし、F#がシームレスに動作するかどうか確認しようともしてない(実際は動作する)。F#で非同期コードを書くのは、C#よりも構文が少ない。ボイラープレートは必要ないし、F#はそういう風に書くべきじゃない。 F#はでかい言語だから、初心者が知らないのは当然だと思う。F#を単純化するんじゃなくて、F#は万人向けじゃないってことを理解すればいいんじゃない? 別に良いんだけど、間違ったことを自信満々に言うより、特定の言語機能について確信がない方がマシだと思う。個人的には、”.NETは〇〇だからクソ”→”いつの話だよ?”→”10年前”→”それが?”みたいな無限ループにイライラする。上の例みたいに、新しいF#のコードでこんなことやるやつはいないと思う。task { … } を普通に使うでしょ。 できるのはわかるけど、コードがクソみたいになるし、MLの美しさが損なわれるって言ってんだよ! マジでやめてくれ。Task CEはF# 6.0からあって、CoreLibのTasksとValueTasksのawaitを問題なく処理できるんだから。 OcamlとF#どっちが好きか、それともC#とF#どっちが好きかってこと?あなたの例は確かにエレガントじゃないけど、 built inされてる関数をわざわざ4行で再現してる時点で設計が悪い。どの言語でもクソコードは書けるでしょ。 F#のcomputation blocksが見た目もっと良ければいいのになー。C#みたいにasyncメソッドでアロー構文もっと綺麗に書けるじゃん?F#って独自のキーワード使いすぎな気がするんだよね。もっと再利用してほしい。member this.Foo = …とかmember val Foo = …とかさ。 批判が支離滅裂で意味不明。そもそもcomputation block使わなくても、組み込み関数で同じことできるし。キーワード増やさないでって言う割に、新しいの提案してるじゃん。F#は万人に合うわけじゃないんだから、それでいいんだよ。 >あんたが新しいキーワードを提案してるって言ってるけど ‘async’はF#のキーワードじゃなくて、builder instanceだよ。return文は何か返したいときにだけ必要。async { let! x = f(); return x}はf()にできる。残りはあんたの意見だね。同意しないけど。 この区別はマジで意味ない。言語の使いやすさの問題なんだよ。純粋関数型言語から抜け出すと、とたんに使いづらくなる。 C#だとasyncじゃないメソッドでawait使えないから、短絡的な意見だと思う。 関係ないじゃん。結局全部asyncにする必要があるけど、それでもF#よりシンタックス綺麗だよ。Asp.Net controller使ってるなら、handlerをasync Task シンタックスは全く同じだよ。C#は えーと、全然同じじゃないよ。let!はcomputation blockの中でしか使えないし。computation blockから値を返して、スレッドを止めずにFunctionalな世界に戻りたいなら、継続を使わないといけないんだけど、C#にはasync/awaitっていう構文糖があるけど、F#にはないんだよね。 awaitはasync functionの中でしか使えないじゃん。let!がcomputation expressionの中でしか使えないのと何が違うのさ? だってasync functionって、動かすためにsyntaxを変える必要ないじゃん。 逆だよ。C#はasync/awaitのsyntaxが固定されてる。F#のasync/awaitのsyntaxは、ユーザーが自由に使える汎用的な仕組みなんだ。 もし君のコードベースがasync awaitを既に使ってるなら、マジで問題ないと思うよ。 君の考えはわかるよ。俺はニッチな言語を学ぶための媒体として見てるんだ。例えば、個人のプロジェクトでRustを使うのが好きなんだよね。リリースしないプロジェクトでも、immutableとか、functional programmingとか、trait-oriented programmingの教訓が日々の仕事に役立つから。だから、ニッチな言語を学ぶのは価値があると思うんだ。 Rustをニッチな言語って呼ぶのはもう違うんじゃない?(2018年くらいならそうかもだけど)多くの大企業で普通に使われてるし。 このバブルの外ではめっちゃニッチだよ。 Stack Overflowのdeveloper surveyによると、Rustは12.5%で、C#やJavaの半分くらい、Pythonの4分の1くらい。Rubyよりは2倍以上。だから全然ニッチじゃない。 念のため言っとくと、そのdeveloper surveyで聞かれたのは、>“過去1年間に集中的に開発作業を行った、または今後1年間に取り組みたいプログラミング、スクリプト、マークアップ言語は何ですか?” 俺的にニッチじゃないってことは仕事があるってことだと思うんだよね。で、Rustには仕事がない。少なくとも意味のある量ではね。ほとんどの国では皆無だよ。だから俺にとってはニッチのど真ん中って感じ。 F#はたぶん永遠にニッチなままだろうね。Rustは違うと思う。MicrosoftとかGoogleとかLinux Kernelでの採用がどんどん増えてるし、加速してるから。CとC++の40年以上の支配を打ち破るには時間がかかるってこと。 個人的にはRustの複雑さよりCのシンプルさの方が好きだな。俺だけかもだけど。 俺はRustの方がCよりずっとシンプルだと思うけどな。コンパイルできれば、たぶんエンコードしたビジネスロジックの有効な表現になってる。もちろん、そのロジックを間違えることはあるし、どの言語もそれが防げるわけじゃない。知ってるって!いろんな言語が試したけど、俺の考えを誤って伝える能力で打ち破ってきたんだ!でも少なくともRustなら、コードが実際に自分が頼んだことをしてくれるって自信がある。Cだと、クラッシュせずに何百回も実行するまでそんな自信は持てないな。 それって…かなり反論的な意見に聞こえるな。Imho、Cは時々ちょっと壊れやすいコードを作っちゃうかもしれないけど、Rustはそれより二桁くらい複雑だと思う。 いや、彼の言いたいことはわかるよ。Cは「時々ちょっと壊れやすいコード」なんかじゃないんだ。コードの複雑さ、コード量、コードに関わる人数が増えれば増えるほど、脆さが蔓延するんだよ。 兄弟コメントが指摘してるように、.fsprojマニフェストとProgram.fsファイルだけだよ。何のボイラープレートのこと言ってるの?JavaとかTypeScriptで作られたプロジェクトとは真逆でしょ。F#の場合、本格的なアプリケーションやライブラリを作るにはプロジェクトが必要だけど、それ以外は.fsxでF#スクリプトを書いて’dotnet fsi {SomeScript.fsx}‘で実行すればいいんだ。 たぶん、新しいプロジェクト形式が登場する前の.NET Core以前のことか、Visual Studioでプロジェクトを作成して、オプションのボックスを全部チェックしてるんじゃないかな。確かに昔は、コードを実行するために必要なボイラープレートが今よりずっと多かった。今は、VS Codeで合計2つのファイルで.NETプロジェクトをかなり快適に実行できるよ。 Visual Studioを使ってるなら、fsprojだけでなくソリューションファイルも生成されるね。俺はC/C++で育ったから、ボイラープレートのプロジェクト/IDE/makeファイルやビルドオブジェクトを見るのは当然だと思ってる。主にJITコンパイル/インタプリタ言語を使ってる人は、ソースファイルがいっぱい入ったディレクトリツリーがあって、CLIツールがすべてを管理してるのに慣れてるんじゃないかな。依存関係リストファイルもあるかもしれないけど、それくらい。Pythonとかjavascriptはそんな感じ。 F#はAIとかなり相性が良いよ。すべてのAIモデルは、自然なF#コードを生成する能力がある。実際、優れた型システムのおかげで、実装する前にAIに型を使って問題をうまくモデル化させれば、ありもしないバグも簡単に見つけられる。 F#とかScala、Haskell、OCamlって、マジでビルドシステムがクソすぎて普及が止まってるよね。Rustの方がビルドシステムが良いから、問題領域に合わなくても選ばれてる。組み込み開発でC++じゃなくてRustを選ぶ理由の8割はビルドシステムだよ。非営利団体とか企業がバックについてる言語が、未だにクソなビルドシステムなのマジ意味不明。ビルドシステムへの投資が一番重要だってマジ。もっとコメントを表示(1)
遅いとしても、runtime modelはすごく柔軟で、スケールアップ・ダウンが安くて、hot updateも簡単だし、非同期処理もたくさんのprocessにまたがってすごく得意だよね。
F#もasync ergonomicsはすごく良いけど、同じようなtask/processingの柔軟性はないし、Websocketsはelixirとかerlangに比べるとちょっと面倒。
Scalaとは相性が悪かったみたい。複雑な言語だって感じて、どうしても理解できなかったんだよね。
F#は評価の最初では最後の選択肢だったし、ScalaはJava ecosystemのおかげで上位だったんだけど、結局うまくいかなかった。
JVM上でF#が動けば最高なんだけどね!
それって何か困ることある?Scalaのコードベースで働く中で、一番良いのはScalaのマルチパラダイムな性質を受け入れてるものだと思うな。OOで全部解決しようとするとレイヤーが増えちゃうし、FPで全部解決しようとすると高度なテクニックが必要になるし。OOの単純な部分とFPの単純な部分を組み合わせる方が、それぞれ単体でやるよりずっと良いと思う。何か違うこと見てきた?
Clojureはほとんど値と関数(といくつかのマクロ)でできてる。クラスとか継承とか情報隠蔽もない。プロトコルとマルチメソッドはあるけどね。良い感じに小さくてパワフルな言語だよ。もっとコメントを表示(2)
var post = await _postService.getById(id);
F#だとこんな感じ。
let getPostById id = async {
let! post = blogPostService.getPostById id
return post
}
let post = getPostById 42 |> Async.RunSynchronously
でも実際は、RunSynchronouslyはawaitと同じじゃないんだよね。async computationの結果をちゃんと扱おうと思ったら、continuation作らないといけないし。F#だけじゃなくて、Ocamlとか他のML系の言語も同じ問題を抱えてる気がする。F#で非同期処理やる時のパターンは、だいたい以下のどっちか。
1.ロジックは全部MLシンタックスで書いて、最後にcomputation blockにデータを渡してI/O処理して、unitを返す。
2.C#みたいなTask<>を返して、I/O処理は全部C#でやる。
どっちにしても、ML系の言語は、俺らが普段やってるCRUDアプリみたいな商用アプリには向いてない気がするんだよね。
> The equivalent C# to your F# would be
>” task { return! _postService.getById(id) }”
これは意味ないし。postServiceからtaskを直接返せばいいじゃん。asyncを同期的に実行する必要もないし。Asyncはtask、thread、sync上で何度もロジックを実行できるから、taskとは全然違うモデル。
C#のコードをF#のコードと比較するには(taskは同じじゃないから厳密には違うけど)、メソッドを定義して、taskを同期的に安全に実行する方法を見つける必要がある。
> public async Task
// これは完全に同じじゃない。taskはhotだから
this.GetPostById(42).Result
> let getById(id:int) : Task
> let doWork(post:string) : unit = failwith ”never”
> let doThing() = task {
> let! post = getById(42);
> doWork(post); }”
または、taskをAsync.AwaitTask関数で通常のF# asyncに変換できる。
> let getPostById1(id:int) : Async
> let getPostById2(id:int) : Async
> let getPostById3 : int -> Async
してないし。asyncはF#のキーワードだし、括弧をなくしてreturn文を削除してほしいって言ってるだけ。
>言語設計にはトレードオフがあるけど、どこが改善されるか分からないって言ってるけど
読みやすくなるし、特定の機能のための特殊な構文が減るし。MLスタイルの構文を維持できるし、computation blockでラップする必要なくなる。あれF#で一番ダサい構文じゃん?
>Computation expressionsは便利なものだって言ってるけど
そうは思わない。見た目が悪い。もっと砂糖をかけてくれ。var post = await _postService.getById(id);
F#だとlet! post = _postService.getById id
だよね。var x = await
で、F#はlet! x =
。controller handlerも同じ。C#はasync
キーワードで、F#はtask
CEでマークされる。もっとコメントを表示(3)
ってこと。
仕事でその言語を使ってるかは聞いてないんだよね。
あと、同じ結果で、Rustのすぐ上に、>PowerShell 13.8%
ってあるし。
<sarcasm>じゃあ、Microsoft PowerShellの方がRustより人気があるって言っても大丈夫だよね。</sarcasm>
(Cの残念なコードを書かないようにするための豊富なエコシステムについてはよく知ってるよ。Coverityで働いてたこともあるし。むしろ、Cのコードが完全に壊れてないことをある程度確信するまでに、どれだけのハードルを乗り越えなきゃいけないかを思い知らされた。)
(当然、shebangとしてdotnet fsiを指定して、これらのスクリプトをUnixシステム上の一般的なスクリプトに統合することもできる。すごく生産的だよ)