LoginSignup
1
2

More than 1 year has passed since last update.

Haskell の Arrow tutorial の Scalaによる試行

Last updated at Posted at 2017-12-30

最近では DDD 関連で語られることもある Functional Reactive Programming (FRP)の分野では、Arrowがよく使われてるという。Arrows-based FRP といった呼び方もあるらしい。

ただ、Scalaのライブラリに入っているのを見たことはあるけど、今まで使ったことがない。そこで今後の予習もかねて、Cats に入っている Arrow で素振りしてみる。お題は、Haskellの Arrow tutorial を選んでみた。

ちなみに自分は初見時やや混同したけど、圏論で「射(morphism)は〜で矢印(arrow)とも呼ばれる」といった文脈での arrow とこのArrow は別のもので(参考)、Freyd-category という圏の一種らしい。ただしここでは深入りせず、monad の generalization としての単なる型クラスの一つといった程度の理解で先に進む。

依存関係など

Scala 3.1.2 と Cats 2.6.1
詳細

1. The Arrow

チュートリアルの「1. The Arrow」では HaskellのControl.Arrowから、arr>>>firstsecond、が紹介されている。これを順に試してみる。

※ Cats では Arrow に関連して以下のような継承関係があること踏まえておく。

Compose    <-- Category <--┐
                           ├─ Arrow
Profunctor <-- Strong   <-─┘ 

arr

arr :: (Arrow a) => (b -> c) -> a b c

Cats ではArrow#liftが対応する。Cats で提供される Arrow[Function] インスタンスを使うと、以下のようになる。

val f1 = Arrow[Function].lift(1d / (_: Int))
f1(10) // res0: Double = 0.1

>>>

(>>>) :: (Arrow a) => a b c -> a c d -> a b d

Haskellの(>>>)は、Cats ではCompose#andThen のエイリアスとして提供される。同様に、これと逆向きの Compose#compose にもエイリアス<<<が提供される。これらはimport cats.syntax.compose._で使えるようになる。

val f2: Int => String = (1d / (_: Int)) >>> (s => s"$s%")
f2(10) // res1: String = 0.1%

first と second

first :: (Arrow a) => a b c -> a (b, d) (c, d)
second :: (Arrow a) => a b c -> a (d, b) (d, c)

firstsecondは、Cats では Strongで定義されている。

val f3: ((Int, String)) => (Double, String) = (1d / (_: Int)).first[String]
f3((10, "ten")) // (0.1, ten)

val f4: ((Int, Double)) => (Int, String) = ((_: Double).toString.reverse).second[Int]
f4((10, 3.14)) // (10, 41.3)

上記 >>>firstsecondを以下のよう組み合わせることもできる。

val f5: ((Int, Double)) => (Double, String) =
  (1d / (_: Int)).first >>> ((_: Double).toString.reverse).second
f5((10, 3.14)) // (0.1, 41.3)

ちなみに Cats では、Function以外にもKleisliCokleisliのインスタンスが定義されており、例えばCokleisliなら以下のように書ける。

def f6(using A: Arrow[[A, B] =>> Cokleisli[NonEmptyList, A, B]]) =
  A.lift[String, Int](_.length)
f6.run(NonEmptyList.of("abc", "ab", "c")) // 3

worksheet

2. A Simple Arrow

「2. A Simple Arrow」ではSimpleFuncという型を定義して、そのArrowインスタンスを作っている。

sealed case class SimpleFunc[A, B](runF: A => B)

given Arrow[SimpleFunc] with 
  def lift[A, B](f: A => B): SimpleFunc[A, B] = SimpleFunc(f)

  def first[A, B, C](fa: SimpleFunc[A, B]): SimpleFunc[(A, C), (B, C)] =
    SimpleFunc((a, c) => (fa runF a , c))

  def compose[A, B, C](g: SimpleFunc[B, C], f: SimpleFunc[A, B]): SimpleFunc[A, C] =
    SimpleFunc(g.runF compose f.runF)

Cats で Arrowを実装すると、チュートリアルの Haskell コードと以下の点で違いが出る。

  • チュートリアルでは別途 Categoryのインスタンスも書いているが、Catsでは ArrowCategoryを継承しているため不要。
  • secondではなくCategory#composeを実装することで自動的にsecondも定まり、逆にCategory#idArrow側liftを実装することで自動的に定まる。

worksheet

3. Some Arrow Operations

「3. Some Arrow Operations」では、Arrowで一般的に使える関数をいくつか定義している。以下順に見てみる。

split / unsplit

以下、チュートリアルのと同等なsplitunsplitのコード。可読性のためとチュートリアルのコードに近づけるためにarrも定義した。

def arr[F[_, _]: Arrow, A, B](f: A => B): F[A, B] = Arrow[F] lift f
def split[F[_, _]: Arrow, A]: F[A, (A, A)]        = arr(x => (x, x))
def unsplit[F[_, _]: Arrow, A, B, C](f: (A, B) => C): F[(A, B), C] =
  arr(f.tupled)

*** / &&&

***Arrow#splitの、&&&Arrow#mergeのエイリアスになっている(名前は同じだが Arrow#split と上で独自実装したsplitは別物。

liftA2

チュートリアルで紹介されている2通りの書き方が、Scalaでも同様に書ける。

def liftA2[F[_, _]: Arrow, A, B, C, D](f: (B, C) => D, fb: F[A, B], fc: F[A, C])
  : F[A, D] = (fb &&& fc) >>> unsplit(f)

def liftA2[F[_, _]: Arrow, A, B, C, D](f: (B, C) => D, fb: F[A, B], fc: F[A, C])
  : F[A, D] = split[F, A] >>> fb.first >>> fc.second >>> unsplit(f)

&&&import cats.syntax.arrow._で使える。
worksheet

4. An Example

「4. An Example」では、ここまでのコードを使ってSimpleFuncを動かしている。Scalaでは以下のようになる。

val f: SimpleFunc[Int, Int] = arr(_ / 2)
val g: SimpleFunc[Int, Int] = arr(_ * 3 + 1)
val plus: (Int, Int) => Int = _ + _

val h = liftA2(plus, f, g)
h runF 8 // : Int = 29

チュートリアルと同様の結果が得られる。
worksheet

5. Kleisli Arrows

KleisliはふだんReaderTとして使うことも多いが、「5. Kleisli Arrows」ではArrowとしての用法が示される。Cats で提供されているKleisliArrowインスタンスを使って同等な Scalaコードを書いた。

type Arr = Kleisli[List, Int, Int]
val plusMinus: Arr = Kleisli(x => x :: (-x) :: Nil)
val double   : Arr = arr(_ * 2)
val h2       : Arr = liftA2((_: Int) + (_: Int), plusMinus, double)

h2 run 8 // : List[Int] = List(24, 8)

計算の流れは 8→(8,8)→([8,-8], 16)→[24, 8]となり、チュートリアルと同じ。

(Haskell の Arrowproc a -> returnA -< aのような構文は現状の Catsには見当たらない。)

worksheet

6. A Teaser

「6. A Teaser」では、HaskellのControl.Arrowで定義されているreturnA<+>を使った応用例が示されている。ここではreturnA<+>も自前で定義した。

def returnA[S] = Arrow[ListKleisli].id[S]

extension [X[_], S](k1: Kleisli[X, S, S])(using M: Semigroup[X[S]])
  def <+>(k2: Kleisli[X, S, S]) = Kleisli((s: S) => k1.run(s) |+| k2.run(s))

main相当は以下。

val arrK = f => arr[ListKleisli, String, String](f)

val prepend = (s: String)                      => arrK(s ++ _)
val append  = (s: String)                      => arrK(_ ++ s)
val withId  = (t: ListKleisli[String, String]) => returnA[String] <+> t

val xform = withId(prepend("<"))
        >>> withId(append(">"))
        >>> withId(prepend("!")
        >>> append("!"))

List("test", "foo") >>= xform.run

チュートリアルにも'important observation'と注意喚起されてるように、結果の組み合わせがちょっと面白い。

test, !test!, test>, !test>!, <test, !<test!, <test>, !<test>!, 
foo, !foo!, foo>, !foo>!, <foo, !<foo!, <foo>, !<foo>!

worksheet

所感・TODOなど

  • 今回はただの型クラスとして触ってみただけだけど、次は Arrow-Based FRPの中での使われ方もみてみる。
  • 冒頭で書いた通り、Arrowは"Freyd-categories"とも呼ばれ、CatsでもCategoryを継承している。数学的な背景もある概念らしいので、モナドとの対比も含めて調べておきたい。
  • Haskell界隈では、GHCの Arrows 拡張で有効になるノーテーションなど表現力の面で語られることが多いようなので、これも Scala でどうなるか調べてみたい。
  • Scala3 を使って書き直した。旧 Scala 版くらべるとかなりスッキリ書けている気がする。 (2022/05/21)
1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2