这一部分主要是Monad Transformers。

Monad transformers

正如我们之前见到的很多类型并不是直接定义的而已通过定义对应的类型名T然后通过type SomeMonad[T] = SomeMonadT[Id, T]而产生的。对于已经存在的类型比如Eitherscalaz也是先构造\/,然后又提供了EitherT这种形式的类型,就称为Monad Transformer。而简单来说,Monad Transformer就提供了Monad组合的能力。通过Monad Transformer组合出来的依旧是Monad而且是有多种功能。

Id

先来看一下这个Id类型到底是什么。其实猜也能猜出来,scalaidentity这个函数代表没什么也不做即x => x,那Id这个类型应该就是什么也不做的类型。

1
2
3
type Id[+X] = X
def point[A](a: => A): A = a
def bind[A, B](a: A)(f: A => B): B = f(a)

可以看出来,Id这个类型并没有给作为参数的类型加入什么功能。就是一个什么也没做的类型。而且,它还是一个Monad

1
2
3
4
(Monad[Id].point(x) >>= f) assert_=== f(x) //等价于 f(x)
(Monad[Id].point(x) >>= Monad[Id].point) assert_=== Monad[Id].point(x) //flatmap id等于id
((Monad[Id].point(x) >>= f) >>= g) assert_===
Monad[Id].point(x) >>= (x => (f(x) >>=g))

这几条都显然成立。所以Id本身就像加法中的0,乘法中的1,在运算中不加入新的功能。

Some example

ReaderT

下面是一个ReaderT的例子

1
2
3
4
5
type ReaderTOption[A, B] = ReaderT[Option, A, B]
object ReaderTOption extends KleisliInstances {
def apply[A, B](f: A => Option[B]): ReaderTOption[A, B] = kleisli(f)
}

然后为了使用这个东西,我们假设,我们要读一个配置的Map由于其中有可能有的项不存在,所以会失败。

先实现获取一个项的函数。

1
2
3
4
5
6
7
8
9
10
11
12
def configure(key: String) = ReaderTOption[Map[String, String], String] {_.get(key)}
def goodConfig = Map (
"user" -> "aaa",
"host" -> "locahost",
"password" -> "******"
)
def badConfig = Map(
"user" -> "aaa",
"host" -> "localhost",
)

这样我们获取内容的时候,就不用每次都处理获取失败的情况了。

1
2
3
4
5
6
7
8
9
10
def setupConnection = for {
host <- configure("host")
user <- configure("user")
passwd <- configure("password")
} yield (host, user, passwd)
setupConnection(goodConfig)
// res0: Option[(String, String, String)] = Some((localhost,aaa,****))
setupConnection(badConfig)
// res1: Option[(String, String, String)] = None

Write your Monad Transformer

Either in scalaz

scalaz提供了自己实现的Either\/Right\/-Left-\/。我们实现一个模拟登陆的代码,同时慢慢实现自己的monad transformer

首先,先定义登陆的错误,其中应该有email格式不合法。同时还要实现,判断的函数。

1
2
3
4
5
6
7
8
9
sealed abstract class LoginError
case object InvalidEmail extends LoginError {
override def toString = "InvalidEmail"
}
def getDomain(email: String): \/[LoginError, String] =
email.split('@') match {
case x if x.length == 2 => x(1).right
case _ => InvalidEmail.left
}

为了把结果打印出来,我们要使用print,这里应该使用IO monad

1
2
3
4
5
def printResult0(domain: \/[LoginError, String]): IO[Unit] =
domain match {
case \/-(text) => putStrLn("Domain: " + text)
case -\/(InvalidEmail) => putStrLn("Error: Invalid domain")
}

最后返回的是IO[Unit],测试的时候可以直接调用IO monadunsafePerformIO

1
2
printResult0(getDomain("aaa@localhost.com")).unsafePerformIO()
// res: Domain: localhost.com

不过这里总写match case感觉不好看,\/提供了fold函数,第一个参数是-\/ => C,第二个参数是\/- => C。所以可以改写成

1
2
3
4
5
def printResult(domain: \/[LoginError, String]): IO[Unit] =
domain.fold(
y => IO.putStrLn(y.toString),
x => IO.putStrLn("Domain: " + x)
)

introducing Side-Effects

现在把整个用户输入的逻辑串联起来由于需要同时封装IO异常两个副作用,所以最后的返回值是IO[LoginError \/ String]]

1
2
3
4
5
def getToken0: IO[\/[LoginError, String]] =
for {
_ <- IO.putStrLn("Enter email address:")
email <- IO.readLn
} yield getDomain(email)

测试一下

1
2
3
getToken0.unsafePerformIO()
// Enter email address:
// res5: scalaz.\/[mt.LoginError,String] = \/-(gmailcom)

然后,我们扩充一下错误类型,并且提供邮箱和密码对应的用例。

1
2
3
4
5
6
7
8
9
10
11
12
val users = Map("example.com"->"qwer", "localhost"->"1234")
sealed abstract class LoginError
case object InvalidEmail extends LoginError {
override def toString = "InvalidEmail"
}
case object NoSuchUser extends LoginError {
override def toString = "NoSuchUser"
}
case object WrongPassword extends LoginError {
override def toString = "WrongPassword"
}

然后把整个输入的逻辑写出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def userLogin0: IO[\/[LoginError, String]] =
for {
token <- getToken0
x <- token match {
case \/-(domain) => {
users.get(domain) match {
case Some(userpw) => for {
_ <- putStrLn("Enter password")
passwd <- readLn
} yield {
if (userpw == passwd) token else WrongPassword.left
}
case None => IO(NoSuchUser.left)
}}
case left => IO(left)
}
} yield x

嗯,这段代码看起来有点乱。首先通过getToken0获取email,然后判断token,如果是正确的就是从user里面取出来password然后对比,不然就直接返回IO(left)。这段代码来起来乱的原因是,两个monad结合在一起的时候,有两层for两个yield再加上match case,这导致代码看起来有点乱。

我们来慢慢简化。

We can make our own Monads

为了解决同时使用两个Monad的尴尬,我们先构建一个新类型,

1
final case class EitherIO[E, A](runEitherIO: IO[\/[E,A]])

然后,将其实现为Monad。在scalaz中,实现Monad是通过提供implicit参数实现的。值得注意的是,我们实现的Monad类型其实是EitherIO[E ,?]而不是EitherIO。所以在声明的时候需要使用类型lambda。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
implicit def eitherIOFunctorInstance[E] =
new Functor[({type l[X] = EitherIO[E, X]})# l]
with Monad[({type l[X] = EitherIO[E, X]})# l]{
override def point[A](a: => A) =
EitherIO[E, A](IO(\/-(a)))
override def map[A, B](em: EitherIO[E, A])(f: A => B): EitherIO[E, B] = {
val unwarpped = em.runEitherIO
val fmapped = unwarpped.map(x => x.map(f))
EitherIO(fmapped)
}
override def bind[A, B](em: EitherIO[E, A])(f: A => EitherIO[E, B]): EitherIO[E, B] =
EitherIO(em.runEitherIO.flatMap(_.fold(
(l:E) => IO(l.left),
(r:A) => f(r).runEitherIO
)))
}

这里使用({type l[X] = EitherIO[E, X]})# l表示EitherIO[E, ?]的类型,然后实现了对应的fmap以及returnbind函数。同时实现了functor以及monad这会使的Monad[EithterIO]获得map,flatmap等函数。

然后我们使用EitherIO修改已有的代码,不过在此之前还有一些问题,我们要重写getToken函数,但是他的类型是EitherIO[LoginError, String],所以我们需要一个能把IO变成EitherIO的函数,我们称为liftIO

1
def liftIO[E, A](e :IO[A]): EitherIO[E, A] = EitherIO(e.map(_.right))

而且,由于getDomain返回的是\/[LoginError, String],所以还需要一个IO[A] => EitherIO[E, A]的函数。我们称为liftEither

1
def liftEither[E, A](e: \/[E, A]): EitherIO[E, A] = EitherIO(IO(e))

修改之后的代码变成

1
2
3
4
5
6
def getToken: EitherIO[LoginError, String] =
for {
_ <- liftIO[LoginError, Unit](putStrLn("Enter email address:"))
email <- liftIO[LoginError, String](readLn)
m <- liftEither[LoginError, String](getDomain(email))
} yield m

接着改写userLogin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def userLogin1: EitherIO[LoginError, String] =
for {
token <- getToken
userpw <- users.get(token).fold(
liftEither[LoginError, String](InvalidEmail.left)
)(
(x: String) => EitherIO[LoginError, String](IO(x.right))
)
passwd <- liftIO[LoginError, String](putStrLn("Enter your password:") >> readLn)
m <- if(passwd != userpw)
liftEither[LoginError, String](-\/(WrongPassword))
else
liftEither[LoginError, String](token.right)
} yield m

这样,这个代码就好看多了。不过由于scala类型推导的问题,lift函数的泛型参数有时候要写全,感觉有点烦。

Going General

其实我们注意到,这里的IO并没有引起太多的特殊处理,我们只要保证它是一个Monad,就能使用其bindpoint,所以我们把它扩展一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
object ExceptT {
implicit def monadInstance[E, M[_]](implicit func: Monad[M]) = new Monad[({type l[A] = ExceptT[E, M, A]})# l] with Functor[({type l[A] = ExceptT[E, M, A]})# l]{
override def point[A](a: => A) = ExceptT[E, M, A](func.point(\/-(a)))
override def map[A, B](et: ExceptT[E, M, A])(f: A => B): ExceptT[E, M, B] = {
val fmapped = func.map(et.runExceptT)(x => x.map(f))
ExceptT(fmapped)
}
override def bind[A, B](et: ExceptT[E, M, A])(f: A => ExceptT[E, M, B]): ExceptT[E, M, B] =
ExceptT(
et.runExceptT.flatMap(_.fold(
(l: E) => func.point(l.left),
(r: A) => f(r).runExceptT
))
)
}
def liftEither[E, M[_], A](et: \/[E, A])(implicit m: Monad[M]): ExceptT[E, M, A] = ExceptT(m.point(et))
def lift[E, M[_], A](et: M[A])(implicit m: Monad[M]): ExceptT[E, M, A] = ExceptT[E, M, A](m.map(et)(_.right[E]))
}

整体和之前的差不多,不过由于scala里面其实没有像haskell那种支持type class的方法,所以即不是用那种继承的语法(这个说法不太准确)来实现type class的,所以为了支持类型约束,依靠的是implicit的参数。所以每个需要约束Monad函数后面都跟一个implicit m。剩下的类型lambda都和之前一样。

Catch

我们再看一下liftEither这个函数,这个函数的类型为Either[Error, A] -> ExceptT[Error, M, A],这是一个用来从错误产生Except的函数,也就是常见的throw。我们改名为throwE并且让它只接受E类型。

1
def throwE[E, M[_], A](e: E)(implicit m: Monad[M]): ExceptT[E, M, A] = liftEither(e.left[A])

那么对称的就应该有catch咯。它应该是一个从给定handler函数,然后处理并且可以接续返回ExceptT的函数。

1
2
3
4
5
6
7
8
9
def catchE[E, M[_], A, C](throwing: => ExceptT[E, M, A])(handler: E => ExceptT[C, M, A])
(implicit m: Monad[M]): ExceptT[C, M, A] =
ExceptT(for {
x <- throwing.runExceptT
r <- (x: \/[E, A]) match {
case \/-(success) => m.point(success.right[C])
case -\/(failure) => handler(failure).runExceptT
}
} yield r)

最后我们完成整个使用的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def wrongPasswordHandler(er: LoginError): ExceptT[LoginError, IO, String] =
er match {
case WrongPassword => for {
_ <- lift[LoginError, IO, Unit](putStrLn("Wrong password, one more chance"))
x <- userLogin
} yield x
case err => throwE(err)
}
def printError[A](err: LoginError): ExceptT[LoginError, IO, A] =
for {
_ <- lift[LoginError, IO, Unit] {
putStrLn {
err match {
case WrongPassword => "Wrong password. No more changes."
case NoSuchUser => "No user with that email exists."
case InvalidEmail => "Invalid email address entered."
}
}
}
x <- throwE[LoginError, IO, A](err)
} yield x
def loginDialogue : ExceptT[LoginError, IO, Unit] =
for {
token <- {
val retry = catchE(userLogin)(wrongPasswordHandler)
catchE(retry)(printError)
}
x <- lift[LoginError, IO, Unit](putStrLn("Logged in with token: " + token))
} yield x

到这里,就完成整个Monad Transformers

引用

这部分基本是参考(翻译)了这个Hasekll的教程

代码在这里




X