Scala ユーザーがぬるっとHaskellに入信したのでHaskellのはじめかたについて書く
Scala is a gateway drug to Haskell
Scala ユーザーが先っちょだけ Haskell に入信してみました.
Scala で 関数型プログラミングと呼ばれるプログラミングスタイルのコードを書きはじめると次第に Haskell(や PureScript などのいわゆる 純粋 関数型言語)の魅力に気が付きますよね(^ω^)? 例えば println
が IO
の外にあるとハァ(´・ω・`)という顔になりますね. cats
を使っていると MonadError
が PureScript と異なって、raise
と handle
の両方の機能を備えているのに困るときがありますよね. (MTL使え、ということでしょうか?)
せっかくなので Haskell をチョット触ってみました. 別にHaskell である必要はないんですが、純粋関数型言語の代名詞のようなものなので Haskell を使ってみることにします.
さて、Haskell をはじめようとすると Scala のアレってどうやるんだっけ、となりますよね. ということでざっくりと Scala と Haskell を対応させながら説明します.
ちなみに ある Haskell ユーザーの方が Scala に対しておおむね良い評価をしてくれているのでうれしいですね(^ω^)
Scala と Haskell の開発環境のざっくりとした対応
大まかにまとめるとこうなります.
scala | haskell | |
---|---|---|
セットアップツール | coursier | ghcup |
ビルドツール | sbt | stack |
language server | metals | hls |
スクリプティング | scala(ammonite) script | stack script |
フォーマッタ/リンター | scalafmt | ormolu,stylish-haskell/hlint などいろいろある. デファクトは?🤔 |
ユースケース | web(play/finagle/doobie/slick他),データ(apack spark/synapseML/breeze/smile), fp(cats/cats-effect/monix/zio) | わからん😖 |
benchmark | sbt-jmh | ??? |
※ 最近の記事だと cable よりも stack をおす声が大きいですがこれは信じて Ok? 🤔
インストール
ghcup & stack
ghcup は Scala の coursier のように haskell の開発環境をよしなにセットアップできます.
stack は sbt と同じように コンパイル、テスト、ベンチマーク、ライブラリのフェッチなど, ビルドタスクを管理するツールです. 環境のセットアップもできるようです.
ghcup
curl で一発.
ghc, stack や hls(haskell language server) をインストールするか聞かれます.
curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh
ghc,ghci(repl),hls などがインストールされます. ちなみに、VS Code の拡張機能をインストールすれば自動で hls がインストールされます. tuiもあるんですね.
とりあえず repl に入ってみます.
ghci
GHCi, version 8.10.7: https://www.haskell.org/ghc/ :? for help
Prelude> putStrLn "hello"
hello
Prelude>:q
GHCi の入力は IO にラップされているんでしょうか.
stack
これも curl で一発.
curl -sSL https://get.haskellstack.org/ | sh
stack --version
Version 2.7.3, Git revision 7927a3aec32e2b2e5e4fb5be76d0d50eddcc197f x86_64 hpack-0.34.4
セットアップ
Scala の coursier setup コマンドに似ています. stack の設定ファイルは ~/.stack
以下に配置されるようです.
stack setup
sbt と stack のざっくりとした対応
sbt | stack | |
---|---|---|
設定ファイル | build.sbt と project/*.scala | stack.yaml |
new | sbt new <project name> /sbt new <template-name> .g8 |
stack new <project name>
|
watch | sbt ~<command name>
|
stack build --watch |
compile | sbt compile | stack build |
run | sbt run | stack exec name または stack run |
test | sbt test | stack test |
設定ファイルが yaml なので、ビルドが複雑になると少し辛そうですね. ライブラリは package.yaml
で管理するようです.
Linux 環境でのインストールはサクサク進みますね(^ω^)
stack script
Haskell を触ってみるにしてもいきなりプロジェクトを作ってゴニョゴニョするのはハードルが高いので(というか特に題材が無いので)まずはスクリプトをシュッと書いてみます. repl や 適当なファイルと ghc を使ってもいいんですが、スクリプトを書くにしてもエディタのサポートが欲しいですね.
Scala ではスクリプティングには ammonite script を使いますが、 Haskell では stack に スクリプティング機能がデフォでついてくるようです.
ファイルに以下のようなヘッダを書くことで Haskell でスクリプトを書くことができるようです.
{-
stack script
--resolver lts-14.18
--install-ghc
--package "<package name>"
--ghc-options <ghc option>
-}
main :: IO ()
main = putStrLn "hello"
Scala の場合はこんな感じになります.
// scala 2.13.7
import $ivy.`com.example::packagename:version`
println("hello")
// or
import $ivy.`com.typelevel::cats-effect:3.3.0`
import cats.effect.IO
import cats.effect.unsafe.implicits.global
val main = IO.println("hello")
main.unsafeRunSync()
なお、少し罠があります. haskell language server は standalone の *.hs
ファイルに対応していないようで、適当な Haskell ファイル一枚を置くだけでは GHC
が見つからないよ、と怒られます. これにハマりました.
以下のような hie.yaml
を追加することでこの問題を解決することができます.
cradle:
stack:
component: "hello.hs"
これで補完やリンターをバリバリ効かせながらスクリプトをかけます. うれしいですね(^ω^)
くらべてみると Haskell は簡潔ですね.
main :: IO ()
main = echo
echo :: IO ()
echo = do
in <- getLine
putStrLn in
Scala はライブラリをフェッチしないと IO は使えません.
import $ivy.`org.typelevel::cats-effect:3.3.0`
import cats.effect.IO
import cats.effect.unsafe.implicits.global
val echo :IO[Unit] = for {
in <- IO.readLine
_ <- IO.println(in)
} yield ()
val main :IO[Unit] = echo
main.unsafeRunSync()
なお、scala のような worksheet 機能は無いみたいです(´・ω・`)
感想
まだ少し触っただけですが sbt と比べて stack はシュッと動いてくれてうれしいですね(^ω^)
(もちろん sbt が"遅い"のは JVM の立ち上げや、常に走らせておく前提があるからですが... また sbtn を使えば2回目以降の呼び出しはシュッとできます.) Linux 環境での開発体験はとてもヨサソウ. Haskell 狂信者のみなさんに感謝.
JVM を入れる必要がないのもインストールのハードルが下がってうれしいですね.
Haskell についてですが、型がしっかりしているので型を見ればだいたい何をすればいいのかわかるので安心です. 型推論が強くて簡潔に書けるのに驚きました. Scala だといちいち IO.pure
というふうに、オブジェクトの関数を呼び出さないといけないところでも Haskell だと自動で推論してくれます. (ただ簡潔になりすぎてチョットそわそわします. 脳内で「これってどの文脈だっけ」とやらないといけないので Scala くらい冗長な方が人間にとって読みやすい気がしないでもないです.)
代数的データ型の定義はHaskell のほうが直感的です. Scala の場合、フィールドの命名もしないといけないのでしばしば うーーん...😖 となります.
data Tree a = Leaf a | Node (Tree a) (Tree a)
sealed trait Tree[+A]
case class Leaf[A](value: A) extends Tree[A]
case class Node[A](left:Tree[A], right:Tree[A]) extends Tree[A]
JavaScript や Ruby などを書いている人からするといきなり IO という方が出てきて最初は何が何だか分からないかもしれませんが、Scala で cats-effect( や cats) のような Haskell にインスパイアされたライブラリを使った経験があれば、概念をなんとなく知っているのでスムーズに書き始められると思います. do 記法、パターンマッチ、関数など、Scala のアレね(知ったかぶり🙄)、という安心感があります.
Scala では(mill というビルドツールもありますが、)ビルドツールはほぼ sbt 一択, フォーマッタは scalafmtがデファクトで、エディタは IntelliJ or Metals & (vs code, vim, emacs, etc), ライブラリは akka, typelevel スタック、lihaoyi エコシステムなどにまとまっています.
一方で、Haskell をはじめようとするとどのツール・ライブラリがデファクトなのかわからないので少し不便ですね. あと、 Scala では infix なオペレータが多いので .
を入力すると候補がズラッと出てきて嬉しいのですが Haskell はそうならない印象があります. 私の使い方がマズいんでしょうか? ご教授願います.
インターネットで調べてみて少し気になるのが、頻繁に使いそうな機能、デフォルトの文字列や Random のパフォーマンスなどに罠が隠れていることですね(解決済みですかね?). どこかにまとまっていると同じ罠を踏まなくてよくなると思います(探せばあるのかな).
あと、プログラミングあるあるかもしれませんが Haskell をインスコした後に途方にくれるので、「これをやると楽しいよ」ってのがあったら教えてほしいですね.
余談
言及したら負けな気がしますが Haskell アレルギー気味の人の「Haskell をやらないためならなんだってやる」ような見苦しい対応を見ると「そういうとこやぞ」という顔になります. その労力(together まとめたり)のいくらかを使えば Haskell(Elm でも PureScript でもいいですが.) をインストールして遊んでみるくらいできるのでは?(#^ω^) 話はそれからだ(^ω^)
ふわっとした定義で「関数型プログラミング」をポジショントークや知識マウントのために使うさまは見たくないですね. そういう不誠実なふるまいをする人のせいでまともな人がコミュニティを去ってしまうのはもったいないです. ポエムやツイートするんじゃなくてコードを書いてほしいな😇
Twitter は議論する場じゃありません&わかりやすい説明をしてもらえば言葉で理解できるほど我々は頭がよくないので手を動かして経験から学びましょう...
あと、Haskell は Haskell で(或いはいわゆる「純粋関数型言語」も)難しいところが抽象化されているので Haskell を書いたら IO や純粋性について理解できるかといわれたら微妙なところかな、と思います. もう少し体系立てて説明している本、 IO モナドに関してはジュース本のハンズオンをやるのがいいのでは?と思いました(小並感) Haskell のヨサソウな本はわからないのでこれも識者にご教授願います😖
Discussion
sbt run
により近いstack run
というのが今はありますstack build --watch
というコマンドでソースコードが変わったときに自動でリビルドさせることができますちょっともう古い本なのでそのまま手順に従えるか分からないのですが、 https://gihyo.jp/book/2017/978-4-7741-9237-6 は割とアプリケーションの実装例が豊富でおすすめです。
デファクトがないつらさですね~😖 ひとまず stack を使ってみて、余裕があったら cabel も試してみます!
がんばります(´・ω・`)
コメントありがとうございます(^ω^)
デファクトスタンダードは残念ながらまだ無いと思います。
コードフォーマッターを定めようとする以前からある古い言語なのでまだバトル中です。
また、ormolu系とhlintは、コードフォーマッタとリンターで別なので、両方併用することをおすすめします。
私はまだstylish-haskellと人力フォーマットに頼っていますが、強いコードフォーマッターを使いたければfourmoluをおすすめします。ormoluのforkであり、カンマの位置とかカスタム出来るので程よく設定可能です。
確かにそういう所はなくはないと思います。
実際トップレベルの関数の引数と返り値とかは型推論させることも出来ますが、注釈を書くことが推奨されています。
私は型から先に書いたり、書いた後にGHCの推論する型を挿入して問題ないことを確認するスタイルの両方を場合によってやりやすい方を使っています。
最近はlspによって変数のその時の型も出てくるので、割と人間にも優しくなったと思います。
worksheetの使いたい用途を学習用にサクッと書いたものを実行したいと解釈します。
以下の方法で代用可能だと思います。
もちろん、stack runとかでもOKですが、依存ライブラリを読み込みつつ、かつ部分的なモジュールの限られた関数を読み出すならこれの方が良いでしょう。
この方法だとexportしていない関数も実行できます。
確かにその通りで、まず規格からある歴史の古い言語の良くない所ですね…
最近はライブラリに関しては、
rio :: Stackage Server
などがある程度まとめていますが、
これも応用的な範囲はあえてカバーしていないので、
webやりたいならServantかなとか自分で調べる必要はありますね。
使い方はまずくないと思います。
最近フィールドの拡張が進んできていますが、
やはり基本的にはデータ型と独立した関数が集まっている感じです。
基本的にドキュメントを見ながら書いたほうが良いと思います。
そのデータ型が定義されているモジュールのドキュメントを読めば、
そのデータ型関連の関数はほとんど一覧で読むことが出来ます。
またHaskellのライブラリのドキュメントは全てHackageに上がっており、
Stackageに認定されてる範囲ならば以下のように型で串刺し検索出来るので、
https://www.stackage.org/lts-18.18/hoogle?q=Int+->+Int+->+Int
私はpackage.yamlに追加されてないものも含めて検索しながら書いています。
これはまさに、
rio :: Stackage Server
である程度カバーしています。
String
をなるべく使わないようになっていたり、文字列をビルダーで効率よく構築できるようにexportがなされています。あえてHaskellの得意な分野を上げるとパーサー周りとかだと思うので、
Megaparsec tutorial from IH book (翻訳)
などを参考に簡単なプログラミング言語を作ってみると楽しいと思います。
その解釈で間違いないですが、 Scala の worksheet だと入力したものを即時に評価してくれたり定義ジャンプができたりとなかなか使い勝手がいいんですよ(^ω^)(宣伝)
ひとまずは紹介していただいた方法を試してみようと思います.
おお、これは知らなかったです. いいですね.
この画像を見て笑いました. Scala の web 系ライブラリもこういうところありますね...
パーサー..!! ヨサソウですね(^ω^) 以前 pandoc のコードを斜め読みしてこんなスッキリかけるんだ~と感動(?)したのでチョットやってみます.
コメントありがとうございます(^ω^) とても参考になりました.