Skip to content

Instantly share code, notes, and snippets.

@lagenorhynque
Last active May 11, 2017 13:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lagenorhynque/3d69afc8eda7f5426d533f65f7190317 to your computer and use it in GitHub Desktop.
Save lagenorhynque/3d69afc8eda7f5426d533f65f7190317 to your computer and use it in GitHub Desktop.

MP in Scala


MP = Monadic Programming


  1. モナドとは何か
  2. モナドの基本的な扱い方
  3. モナド変換子の使い方

自己紹介

lagenorhynchus


(defprofile
  大橋 賢人 [OHASHI Kent]
  :company   株式会社オプト  テクノロジー開発
  :github    @lagenorhynque
  :twitter   @lagenorhynque
  :languages [Python Haskell Clojure Scala English français Deutsch русский]
  :interests [プログラミング 語学 数学])

モナド(Monad)とは


一言で言えば、 flatMap できる型クラス(type class)。

F[A] => (A => F[B]) => F[B]

型クラスFunctor, Applicativeの上位互換。

// scalaz.Monad
trait Monad[F[_]] extends Applicative[F] with Bind[F] {
  abstract def bind[A, B](fa: F[A])(f: (A)  F[B]): F[B]
  abstract def point[A](a:  A): F[A]
  ...
}
-- Haskell
class Applicative m => Monad m where
  (>>=) :: forall a b. m a -> (a -> m b) -> m b
  return :: a -> m a
  ...

モナドの具体例


例えば、 Option

標準ライブラリでは型クラスとして実装されているわけではないが

Functor, Applicative, Monadに相当する機能を備えている。


Functorの map


F[A] => (A => B) => F[B]

scalaz.Functor.map にあたる。

val n: Option[Int] = Some(1)
val f: Int => String = x => x.toString

// Option#map
n.map(f)
// Option#flatMap による実装
n.flatMap { a =>
  Some(f(a))
}
// for式による実装
for {
  a <- n
} yield f(a)

Applicativeの ap / <*>


F[A] => F[A => B] => F[B]

scalaz.Apply.ap, scalaz.syntax.ApplyOps.<*> にあたる。

val n: Option[Int] = Some(1)
val f: Option[Int => String] = Some(x => x.toString)

// Option#flatMap, Option#map による実装
n.flatMap { a =>
  f.map { g =>
    g(a)
  }
}
// for式による実装
for {
  a <- n
  g <- f
} yield g(a)

Monadの flatMap / bind / >>=


F[A] => (A => F[B]) => F[B]

scalaz.Bind.bind, scalaz.syntax.BindOps.>>= にあたる。

val n: Option[Int] = Some(1)
val f: Int => Option[String] = x => Some(x.toString)

// Option#flatMap
n.flatMap(f)
// for式による実装
for {
  a <- n
  b <- f(a)
} yield b

同種のモナドを扱う場合


例えば、 Option を単独で使う場合


同種のモナドを扱う場合

(1) パターンマッチ


構造に注目して分解(unapply, destructure)する。

case class User(firstName: Option[String], lastName: Option[String])

def userName(user: User): Option[String] = user match {
  case User(Some(first), Some(last)) => Some(s"$first $last")
  case _ => None
}

同種のモナドを扱う場合

(2) 高階関数


高階関数 map, flatMap, etc.を組み合わせる。

case class User(firstName: Option[String], lastName: Option[String])

def userName(user: User): Option[String] =
  user.firstName.flatMap { first =>
    user.lastName.map { last =>
      s"$first $last"
    }
  }

同種のモナドを扱う場合

(3) for


モナドのためのシンタックスシュガーを活用する。

case class User(firstName: Option[String], lastName: Option[String])

def userName(user: User): Option[String] =
  for {
    first <- user.firstName
    last <- user.lastName
  } yield s"$first $last"

異種のモナドが混在する場合


例えば、 Optionscalaz.\/ を組み合わせて

E \/ Option[A] として扱う必要がある場合


異種のモナドが混在する場合

(1) パターンマッチ


import scalaz._, Scalaz._

case class User(id: Int,
                firstName: Option[String],
                lastName: Option[String])
def userRole(id: Int): Error \/ Role = ???

def userInfo(user: User): Error \/ Option[String] =
  userRole(user.id) match {
    case \/-(role) => user match {
      case User(_, Some(first), Some(last)) =>
        \/-(Some(s"$first $last: $role"))
      case _ =>
        \/-(None)
    }
    case -\/(error) => -\/(error)
  }

問題点


  • 複数階層のモナドの分解・再構築が煩わしい
  • 構造に強く依存しているため変更に弱い
  • パターンマッチがネストして書きづらく読みづらい

異種のモナドが混在する場合

(2) 高階関数


import scalaz._, Scalaz._

case class User(id: Int,
                firstName: Option[String],
                lastName: Option[String])
def userRole(id: Int): Error \/ Role = ???

def userInfo(user: User): Error \/ Option[String] =
  userRole(user.id).map { role =>
    user.firstName.flatMap { first =>
      user.lastName.map { last =>
        s"$first $last: $role"
      }
    }
  }

問題点


  • 構造を直接扱う必要はないが関数がネストして書きづらく読みづらい

異種のモナドが混在する場合

(3) for


import scalaz._, Scalaz._

case class User(id: Int,
                firstName: Option[String],
                lastName: Option[String])
def userRole(id: Int): Error \/ Role = ???

def userInfo(user: User): Error \/ Option[String] =
  for {
    role <- userRole(user.id)
  } yield for {
    first <- user.firstName
    last <- user.lastName
  } yield s"$first $last: $role"

問題点


  • 関数はネストしないがfor式が連鎖して書きづらく読みづらい

モナド変換子(monad transformer)とは


一言で言えば、あるモナドに別のモナドを上乗せ(合成)したモナド。

ネストしたモナドをネストしていないかのように扱えるようになる。

e.g. scalaz.OptionT, scalaz.EitherT, scalaz.ListT


モナド変換子の生成と変換


// 型パラメータを1個とる型F(モナド)
type F[A] = ???

// FとOptionでネストしたモナド
val fOptionA: F[Option[A]] = ???
// OptionとFを合成したOptionT
val optionTFA: OptionT[F, A] = ???
// Option
val optionA: Option[A] = ???
// F
val fA: F[A] = ???

// F[Option[A]] → OptionT[F, A]
OptionT.optionT(fOptionA)
// OptionT[F, A] → F[Option[A]]
optionTFA.run
// Option[A] → F[Option[A]] → OptionT[F, A]
OptionT.optionT(optionA.point[F])
// F[A] → OptionT[F, A]
fA.liftM[OptionT]

モナド変換子の導入


ここではモナド変換子 scalaz.OptionT を利用して

Optionscalaz.\/ を合成してみる。


import scalaz._, Scalaz._

case class User(id: Int,
                firstName: Option[String],
                lastName: Option[String])
def userRole(id: Int): Error \/ Role = ???

type ErrorOrResult[+A] = Error \/ A

def userInfo(user: User): Error \/ Option[String] =
  (for {
    role <- userRole(user.id).liftM[OptionT]
    first <- OptionT.optionT(user.firstName.point[ErrorOrResult])
    last <- OptionT.optionT(user.lastName.point[ErrorOrResult])
  } yield s"$first $last: $role").run

  • Option[+A]E \/ Afor式1つでシンプルに扱える

  • モナド変換子への変換がやや冗長


さらにリファクタ


モナド変換子への変換を関数として抽出してみる。


import scalaz._, Scalaz._

case class User(id: Int,
                firstName: Option[String],
                lastName: Option[String])
def userRole(id: Int): Error \/ Role = ???

type ErrorOrResult[+A] = Error \/ A
def fromOption[A](a: Option[A]): OptionT[ErrorOrResult, A] =
  OptionT.optionT(a.point[ErrorOrResult])
def fromEither[A](a: ErrorOrResult[A]): OptionT[ErrorOrResult, A] =
  a.liftM[OptionT]

def userInfo(user: User): Error \/ Option[String] =
  (for {
    role <- userRole(user.id) ▹ fromEither
    first <- user.firstName ▹ fromOption
    last <- user.lastName ▹ fromOption
  } yield s"$first $last: $role").run

ちなみに


Q: このtype aliasは何のためにあるのか?

type ErrorOrResult[+A] = Error \/ A

A: モナド変換子の型パラメータにカインド(kind)を合わせるため。


カインド
OptionT[F[_], A]の**F[_]** * -> *
\/[+A, +B] * -> * -> *

// 方法1: type aliasする
type ErrorOrResult[+A] = Error \/ A
OptionT[ErrorOrResult, A]

// 方法2: インラインでtype aliasする(type lambda)
OptionT[({type λ[+α] = Error \/ α})#λ, A]

// 方法3: コンパイラプラグインKind Projectorを利用する
OptionT[Error \/ +?, A]

Further Reading

Source Code

MP in Haskell


MP = Monadic Programming


  1. モナドとは何か
  2. モナドの基本的な扱い方
  3. モナド変換子の使い方

自己紹介

lagenorhynchus


(defprofile
  大橋 賢人 [OHASHI Kent]
  :company   株式会社オプト  テクノロジー開発
  :github    @lagenorhynque
  :twitter   @lagenorhynque
  :languages [Python Haskell Clojure Scala English français Deutsch русский]
  :interests [プログラミング 語学 数学])

モナド(Monad)とは


一言で言えば、 >>= (bind) できる型クラス(type class)。

m a -> (a -> m b) -> m b

型クラスFunctor, Applicativeの上位互換。

-- Haskell
class Applicative m => Monad m where
  (>>=) :: forall a b. m a -> (a -> m b) -> m b
  return :: a -> m a
  ...
// scalaz.Monad
trait Monad[F[_]] extends Applicative[F] with Bind[F] {
  abstract def bind[A, B](fa: F[A])(f: (A)  F[B]): F[B]
  abstract def point[A](a:  A): F[A]
  ...
}

モナドの具体例


例えば、 Maybe

型クラスFunctor, Applicative, Monadのインスタンスになっている。


Functorの fmap / <$>


(a -> b) -> f a -> f b

n :: Maybe Int
n = Just 1
f :: Int -> String
f = \x -> show x

-- fmap
fmap f n
-- <$>
f <$> n
-- >>= による実装
n >>= return . f
-- do記法による実装
do
  a <- n
  return $ f a

Applicativeの <*>


f (a -> b) -> f a -> f b

n :: Maybe Int
n = Just 1
f :: Maybe (Int -> String)
f = Just $ \x -> show x

-- <*>
f <*> n
-- >>= による実装
n >>= \a ->
  f >>= \g ->
    return $ g a
-- do記法による実装
do
  a <- n
  g <- f
  return $ g a

Monadの >>=


m a -> (a -> m b) -> m b

n :: Maybe Int
n = Just 1
f :: Int -> Maybe String
f = \x -> Just $ show x

-- >>=
n >>= f
-- do記法による実装
do
  a <- n
  f a

同種のモナドを扱う場合


例えば、 Maybe を単独で使う場合


同種のモナドを扱う場合

(1) パターンマッチ


構造に注目して分解(unapply, destructure)する。

data User = User {
  firstName :: Maybe String,
  lastName :: Maybe String
} deriving Show

userName :: User -> Maybe String
userName User {firstName = Just f, lastname = Just l} = Just $ f ++ " " ++ l
userName _                                            = Nothing

同種のモナドを扱う場合

(2) 高階関数


高階関数 >>=, etc.を組み合わせる。

data User = User {
  firstName :: Maybe String,
  lastName :: Maybe String
} deriving Show

userName :: User -> Maybe String
userName user =
  firstName user >>= \f ->
    lastName user >>= \l ->
      return $ f ++ " " ++ l

同種のモナドを扱う場合

(3) do記法


モナドのためのシンタックスシュガーを活用する。

data User = User {
  firstName :: Maybe String,
  lastName :: Maybe String
} deriving Show

userName :: User -> Maybe String
userName user = do
  f <- firstName user
  l <- lastName user
  return $ f ++ " " ++ l

異種のモナドが混在する場合


例えば、 MaybeEither を組み合わせて

Either e (Maybe a) として扱う必要がある場合


異種のモナドが混在する場合

(1) パターンマッチ


data User = User {
  id' :: Int,
  firstName :: Maybe String,
  lastName :: Maybe String
} deriving Show

userRole :: Int -> Either Error Role
userRole i = undefined

userInfo :: User -> Either Error (Maybe String)
userInfo user = case userRole $ id' user of
  Right r -> case user of
    User {firstName = Just f, lastName = Just l} ->
      Right . Just $ f ++ " " ++ l ++ ": " ++ show r
    _ ->
      Right Nothing
  Left e -> Left e

問題点


  • 複数階層のモナドの分解・再構築が煩わしい
  • 構造に強く依存しているため変更に弱い
  • パターンマッチがネストして書きづらく読みづらい

異種のモナドが混在する場合

(2) 高階関数


data User = User {
  id' :: Int,
  firstName :: Maybe String,
  lastName :: Maybe String
} deriving Show

userRole :: Int -> Either Error Role
userRole i = undefined

userInfo :: User -> Either Error (Maybe String)
userInfo user =
  userRole (id' user) >>= \r ->
    return $ firstName user >>= \f ->
      lastName user >>= \l ->
        return $ f ++ " " ++ l ++ ": " ++ show r

問題点


  • 構造を直接扱う必要はないが関数がネストして書きづらく読みづらい

異種のモナドが混在する場合

(3) do記法


data User = User {
  id' :: Int,
  firstName :: Maybe String,
  lastName :: Maybe String
} deriving Show

userRole :: Int -> Either Error Role
userRole i = undefined

userInfo :: User -> Either Error (Maybe String)
userInfo user = do
  r <- userRole $ id' user
  return $ do
  f <- firstName user
  l <- lastName user
  return $ f ++ " " ++ l ++ ": " ++ show r

問題点


  • 関数はネストしないがdo記法が連鎖して書きづらく読みづらい

モナド変換子(monad transformer)とは


一言で言えば、あるモナドに別のモナドを上乗せ(合成)したモナド。

ネストしたモナドをネストしていないかのように扱えるようになる。

e.g. MaybeT, EitherT, ListT


モナド変換子の生成と変換


-- M(モナド)とMaybeでネストしたモナド
mMaybeA :: M (Maybe a)
-- MaybeとMを合成したMaybeT
maybeTMA :: MaybeT M a
-- Maybe
maybeA :: Maybe a
-- M
mA :: M a

-- M (Maybe a) → MaybeT M a
MaybeT mMaybeA
-- MaybeT M a → M (Maybe a)
runMaybeT maybeTMA
-- Maybe a → M (Maybe a) → MaybeT M a
MaybeT $ return maybeA
-- M a → MaybeT M a
lift mA

モナド変換子の導入


ここではモナド変換子 MaybeT を利用して

MaybeEither を合成してみる。


import Control.Monad.Trans.Class
import Control.Monad.Trans.Maybe

data User = User {
  id' :: Int,
  firstName :: Maybe String,
  lastName :: Maybe String
} deriving Show

userRole :: Int -> Either Error Role
userRole i = undefined

userInfo :: User -> Either Error (Maybe String)
userInfo user = runMaybeT $ do
  r <- lift . userRole $ id' user
  f <- MaybeT . return $ firstName user
  l <- MaybeT . return $ lastName user
  return $ f ++ " " ++ l ++ ": " ++ show r

  • Maybe aEither e ado記法1つでシンプルに扱える

  • モナド変換子への変換がやや冗長


さらにリファクタ


モナド変換子への変換を関数として抽出してみる。


import Control.Monad.Trans.Class
import Control.Monad.Trans.Maybe

data User = User {
  id' :: Int,
  firstName :: Maybe String,
  lastName :: Maybe String
} deriving Show

userRole :: Int -> Either Error Role
userRole i = undefined

fromMaybe :: Maybe a -> MaybeT (Either Error) a
fromMaybe = MaybeT . return

userInfo :: User -> Either Error (Maybe String)
userInfo user = runMaybeT $ do
  r <- lift . userRole $ id' user
  f <- fromMaybe $ firstName user
  l <- fromMaybe $ lastName user
  return $ f ++ " " ++ l ++ ": " ++ show r

Further Reading

Source Code

module MonadTransformers where
import Control.Monad.Trans.Class
import Control.Monad.Trans.Maybe
data User = User {
id' :: Int,
firstName :: Maybe String,
lastName :: Maybe String
} deriving Show
type Error = String
type Role = Int
userRole :: Int -> Either Error Role
userRole 0 = Left "ダメですっ!"
userRole _ = Right 25252
userInfo :: User -> Either Error (Maybe String)
userInfo user = runMaybeT $ do
r <- lift . userRole $ id' user
f <- MaybeT . return $ firstName user
l <- MaybeT . return $ lastName user
return $ f ++ " " ++ l ++ ": " ++ show r
import scalaz._, Scalaz._
object MonadTransformers {
case class User(id: Int,
firstName: Option[String],
lastName: Option[String])
type Error = String
type Role = Int
def userRole(id: Int): Error \/ Role = id match {
case 0 => "ダメですっ!".left
case _ => 25252.right
}
type ErrorOrResult[+A] = Error \/ A
def userInfo(user: User): Error \/ Option[String] =
(for {
role <- userRole(user.id).liftM[OptionT]
first <- OptionT.optionT(user.firstName.point[ErrorOrResult])
last <- OptionT.optionT(user.lastName.point[ErrorOrResult])
} yield s"$first $last: $role").run
}
#!/usr/bin/env bash
# npm install -g reveal-md@0.0.32
reveal-md monad-transformers.md --theme night --highlightTheme monokai-sublime
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment