🥰

Scala ユーザーがぬるっとHaskellに入信したのでHaskellのはじめかたについて書く

2021/12/06に公開
4

Scala is a gateway drug to Haskell

Scala ユーザーが先っちょだけ Haskell に入信してみました.

Scala で 関数型プログラミングと呼ばれるプログラミングスタイルのコードを書きはじめると次第に Haskell(や PureScript などのいわゆる 純粋 関数型言語)の魅力に気が付きますよね(^ω^)? 例えば printlnIO の外にあるとハァ(´・ω・`)という顔になりますね. cats を使っていると MonadError が PureScript と異なって、raisehandle の両方の機能を備えているのに困るときがありますよね. (MTL使え、ということでしょうか?)

せっかくなので Haskell をチョット触ってみました. 別にHaskell である必要はないんですが、純粋関数型言語の代名詞のようなものなので Haskell を使ってみることにします.

さて、Haskell をはじめようとすると Scala のアレってどうやるんだっけ、となりますよね. ということでざっくりと Scala と Haskell を対応させながら説明します.

ちなみに ある Haskell ユーザーの方が Scala に対しておおむね良い評価をしてくれているのでうれしいですね(^ω^)

https://www.ncaq.net/2020/11/18/19/02/59/

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 でスクリプトを書くことができるようです.

hello.hs
{-
  stack script
  --resolver lts-14.18
  --install-ghc
  --package "<package name>"
  --ghc-options <ghc option>
-}
main :: IO ()
main = putStrLn "hello"

Scala の場合はこんな感じになります.

hello.sc
// 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 は簡潔ですね.

echo.hs
main :: IO ()
main = echo

echo :: IO ()
echo  = do
  in <- getLine
  putStrLn in

Scala はライブラリをフェッチしないと IO は使えません.

echo.sc
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 のヨサソウな本はわからないのでこれも識者にご教授願います😖

https://www.amazon.co.jp/Scala関数型デザイン-プログラミング-―Scalazコントリビューターによる関数型徹底ガイド-impress-gear/dp/4844337769

Discussion

YAMAMOTO YujiYAMAMOTO Yuji
  • sbt run により近い stack run というのが今はあります
  • stack build --watch というコマンドでソースコードが変わったときに自動でリビルドさせることができます
  • 個人的には https://the.igreque.info/slides/2019-11-29-stack-cabal.html#(1) にも書いたとおり、cabalの問題点を解決するためにstackが生まれたけどここ数年はcabalもかなりよくなったので、cabalもおすすめです。どちらか選べ、と言われると個人的にはcabalを勧めたいのですが、stackの方がエラーメッセージが易しくとっつきやすい、などのメリットがあり、特に初心者の方が相手だと悩ましいです
  • デフォルトの文字列は今も場面に応じて選ぶしかないですねぇ😥
  • randomパッケージについてはバージョン1.2で劇的に改善されたのでとりあえず1.2以降を使えばいいと思います。(詳細: https://zenn.dev/autotaker/articles/random-1-2-is-awesome

あと、プログラミングあるあるかもしれませんが Haskell をインスコした後に途方にくれるので、「これをやると楽しいよ」ってのがあったら教えてほしいですね.

ちょっともう古い本なのでそのまま手順に従えるか分からないのですが、 https://gihyo.jp/book/2017/978-4-7741-9237-6 は割とアプリケーションの実装例が豊富でおすすめです。

110416110416
  • stack bulid --watch : これは書いていたんですが、markdown をタイポして消えていました. 指摘ありがとうございます。

cabalもおすすめです。どちらか選べ、と言われると個人的にはcabalを勧めたいのですが、stackの方がエラーメッセージが易しくとっつきやすい、などのメリットがあり

デファクトがないつらさですね~😖 ひとまず stack を使ってみて、余裕があったら cabel も試してみます!

デフォルトの文字列は今も場面に応じて選ぶしかないですねぇ

がんばります(´・ω・`)

コメントありがとうございます(^ω^)

エヌユルエヌユル

ormolu,stylish-haskell/hlint などいろいろある. デファクトは?🤔

デファクトスタンダードは残念ながらまだ無いと思います。
コードフォーマッターを定めようとする以前からある古い言語なのでまだバトル中です。
また、ormolu系とhlintは、コードフォーマッタとリンターで別なので、両方併用することをおすすめします。
私はまだstylish-haskellと人力フォーマットに頼っていますが、強いコードフォーマッターを使いたければfourmoluをおすすめします。ormoluのforkであり、カンマの位置とかカスタム出来るので程よく設定可能です。

ただ簡潔になりすぎてチョットそわそわします. 脳内で「これってどの文脈だっけ」とやらないといけないので Scala くらい冗長な方が人間にとって読みやすい気がしないでもないです.

確かにそういう所はなくはないと思います。
実際トップレベルの関数の引数と返り値とかは型推論させることも出来ますが、注釈を書くことが推奨されています。
私は型から先に書いたり、書いた後にGHCの推論する型を挿入して問題ないことを確認するスタイルの両方を場合によってやりやすい方を使っています。
最近はlspによって変数のその時の型も出てくるので、割と人間にも優しくなったと思います。

なお、scala のような worksheet 機能は無いみたいです(´・ω・`)

worksheetの使いたい用途を学習用にサクッと書いたものを実行したいと解釈します。
以下の方法で代用可能だと思います。

  1. Haskellソースをファイルに書きます。
  2. ghciでloadします。(例えばEmacsのhaskell-modeではhaskell-process-load-fileというコマンドがあり、これを実行することで即座にghciのバッファに読み込めます。)
  3. ghciのプロンプトで実行したい関数を入力してエンターします。

もちろん、stack runとかでもOKですが、依存ライブラリを読み込みつつ、かつ部分的なモジュールの限られた関数を読み出すならこれの方が良いでしょう。
この方法だとexportしていない関数も実行できます。

一方で、Haskell をはじめようとするとどのツール・ライブラリがデファクトなのかわからないので少し不便ですね.

確かにその通りで、まず規格からある歴史の古い言語の良くない所ですね…
最近はライブラリに関しては、
rio :: Stackage Server
などがある程度まとめていますが、
これも応用的な範囲はあえてカバーしていないので、
webやりたいならServantかなとか自分で調べる必要はありますね。

あと、 Scala では infix なオペレータが多いので . を入力すると候補がズラッと出てきて嬉しいのですが Haskell はそうならない印象があります. 私の使い方がマズいんでしょうか? ご教授願います.

使い方はまずくないと思います。
最近フィールドの拡張が進んできていますが、
やはり基本的にはデータ型と独立した関数が集まっている感じです。

基本的にドキュメントを見ながら書いたほうが良いと思います。
そのデータ型が定義されているモジュールのドキュメントを読めば、
そのデータ型関連の関数はほとんど一覧で読むことが出来ます。
またHaskellのライブラリのドキュメントは全てHackageに上がっており、
Stackageに認定されてる範囲ならば以下のように型で串刺し検索出来るので、
https://www.stackage.org/lts-18.18/hoogle?q=Int+->+Int+->+Int
私はpackage.yamlに追加されてないものも含めて検索しながら書いています。

インターネットで調べてみて少し気になるのが、頻繁に使いそうな機能、デフォルトの文字列や Random のパフォーマンスなどに罠が隠れていることですね(解決済みですかね?). どこかにまとまっていると同じ罠を踏まなくてよくなると思います(探せばあるのかな).

これはまさに、
rio :: Stackage Server
である程度カバーしています。
Stringをなるべく使わないようになっていたり、文字列をビルダーで効率よく構築できるようにexportがなされています。

あと、プログラミングあるあるかもしれませんが Haskell をインスコした後に途方にくれるので、「これをやると楽しいよ」ってのがあったら教えてほしいですね.

あえてHaskellの得意な分野を上げるとパーサー周りとかだと思うので、
Megaparsec tutorial from IH book (翻訳)
などを参考に簡単なプログラミング言語を作ってみると楽しいと思います。

110416110416

強いコードフォーマッターを使いたければfourmoluをおすすめします。

worksheetの使いたい用途を学習用にサクッと書いたものを実行したいと解釈します。

その解釈で間違いないですが、 Scala の worksheet だと入力したものを即時に評価してくれたり定義ジャンプができたりとなかなか使い勝手がいいんですよ(^ω^)(宣伝)

ひとまずは紹介していただいた方法を試してみようと思います.

rio

おお、これは知らなかったです. いいですね.
この画像を見て笑いました. Scala の web 系ライブラリもこういうところありますね...

あえてHaskellの得意な分野を上げるとパーサー周りとかだと思うので、

パーサー..!! ヨサソウですね(^ω^) 以前 pandoc のコードを斜め読みしてこんなスッキリかけるんだ~と感動(?)したのでチョットやってみます.

コメントありがとうございます(^ω^) とても参考になりました.