Skip to content

Instantly share code, notes, and snippets.

@julienrf
Forked from paul-r-ml/reader-applicative-monadic.rb
Created December 8, 2011 21:00
Show Gist options
  • Save julienrf/1448561 to your computer and use it in GitHub Desktop.
Save julienrf/1448561 to your computer and use it in GitHub Desktop.
simple reader monad
-- Un type qui sera une instance de Monad. Il s'agit d'un simple
-- conteneur pour les fonctions d'un environnement vers un résultat.
newtype Reader env res = Reader (env -> res)
-- Une fonction qui prend un Reader et un environnement, et qui
-- renvoit le résultat
runReader :: Reader env res -> env -> res
runReader (Reader f) e = f e
-- La fameuse instance de Monad. return ignore son environnement,
-- c'est en fait le conteneur de la fonction constante. bind execute
-- la fonction en lui fournissant l'environnement.
instance Monad (Reader env) where
return x = Reader $ \_ -> x
(Reader f) >>= g = Reader $ \env -> runReader (g $ f env) env
-- Obtenir le contenu de l'environnement comme résultat
ask :: Reader e e
ask = Reader id
-- Utilisation
data Person = Person { name :: String, age :: Int }
julien :: Person
julien = Person "Julien" 25
-- affiche le résultat d'une fonction qui construit la présentation
main :: IO ()
main = putStrLn $ show $ runReader presentation julien
-- tu m'as dit que tu aimais les "do" alors en voila ...
presentation :: Reader Person String
presentation = do
namePart <- getNamePart
agePart <- getAgePart
return $ concat [namePart, ", ", agePart, "."]
-- les sous-parties, sans le 'do' ce coup-ci pour changer
getNamePart, getAgePart :: Reader Person String
getNamePart = ask >>= \p ->
return $ concat ["Bonjour je m'appelle ", name p]
getAgePart = ask >>= \p ->
return $ concat ["et j'ai ", show $ age p, " ans"]
-- Quand le programme est executé, il affiche "Bonjour je m'appelle
-- Julien, et j'ai 25 ans."
package reader
object Main extends App {
// Le type Reader, et ses méthodes map et flatMap, nécessaires pour être utilisées dans la notation “monad-comprehension”
class Reader[Env, Res](val read: Env => Res) {
def map[A](g: Res => A): Reader[Env, A] = Reader((env: Env) => g(f(env)))
// Tu remarqueras que cette fonction est équivalente à “bind”
def flatMap[A](g: Res => Reader[Env, A]): Reader[Env, A] = Reader((env: Env) => g(f(env)) read env)
}
object Reader {
// L’équivalent de “pure”. Cette fonction est appelée quand j’écris “Reader(…)” (sans le “new”)
def apply[E, R](f: E => R) = new Reader(f)
}
// Utilisation
case class Person(name: String, age: Int)
val paul: Person = Person("Paul", 26)
// Appelle la méhode “read” du Reader construit par la fonction “presentation”
println(presentation read paul)
// Enfin, la notation monad-comprehension !
// En fait c’est juste un sucre syntaxique pour:
// “getNamePart.flatMap(namePart => getAgePart.map(agePart => namePart + ", " + agePart + "."))”
def presentation: Reader[Person, String] = {
for {
namePart <- getNamePart
agePart <- getAgePart
} yield namePart + ", " + agePart + "."
}
// Construit un Reader qui fournit une String à partir d’une Person
def getNamePart = Reader { (p: Person) => "Bonjour je m’appelle " + p.name }
def getAgePart = Reader { (p: Person) => "et j’ai " + p.age + " ans" }
}
// Au final, le programme affiche : « Bonjour je m’appelle Paul, et j’ai 26 ans. »
object Main extends App {
// Un conteneur de fonction de Env vers Res
class Reader[Env, Res](val f: Env => Res)
// Prend un Reader et un environnement et renvoie le résultat de l’application du Reader
def runReader[E, R](reader: Reader[E, R], env: E) = reader.f(env)
// La typeclass Monad, car elle n’est pas prédéfinie dans Scala
// Tu peux voir https://github.com/scalaz/scalaz pour une bibliothèque qui implémente la plupart des typeclasses de Haskell en Scala
trait Monad[M[_]] {
def pure[A](x: A): M[A]
def bind[A, B](m: M[A])(f: A => M[B]): M[B] // J’ai mis “bind” plutôt que “>>=” car cette fonction ne sera pas utilisée en syntaxe infixe
}
// Un type qui me facilitera l’écriture de l’application partielle du type Reader
trait ReaderE[E] {
type λ[X] = Reader[E, X]
}
// L’instance de Monad
implicit def ReaderMonad[E] = new Monad[ReaderE[E]#λ] {
def pure[A](x: A) = new Reader[E, A]((_: E) => x)
def bind[A, B](reader: Reader[E, A])(g: A => Reader[E, B]): Reader[E, B] =
new Reader((env: E) => runReader(g(reader.f(env)), env))
}
def ask[E]: Reader[E, E] = new Reader[E, E](e => e)
// Utilisation
case class Person(name: String, age: Int)
val paul: Person = Person("Paul", 26)
// Affiche le résultat de la fonction presentation
println(runReader(presentation, paul))
// Je ne peux malheureusement pas utiliser la notation “monad-comprehension” directement ici car
// elle n’est pas intégrée avec le style purement fonctionnel en Scala
def presentation(implicit m: Monad[ReaderE[Person]#λ]): Reader[Person, String] = {
m.bind (getNamePart) { namePart =>
m.bind (getAgePart) { agePart =>
m pure (namePart + ", " + agePart + ".")
}
}
}
def getNamePart(implicit m: Monad[ReaderE[Person]#λ]): Reader[Person, String] =
m.bind (ask[Person]) { p: Person => m pure ("Bonjour je m’appelle " + p.name) }
def getAgePart(implicit m: Monad[ReaderE[Person]#λ]): Reader[Person, String] =
m.bind (ask[Person]) { p: Person => m pure ("et j’ai " + p.age + " ans") }
}
// Au final, le programme affiche : « Bonjour je m’appelle Paul, et j’ai 26 ans. »
@paul-r-ml
Copy link

Ah c'est excellent ! Ça se lit super bien en effet, c'est sympa de voir que ce genre de concept passe bien d'un langage à un autre.
Par rapport à tes remarques sur https://gist.github.com/1443519#gistcomment-68113 :

  • ask n'est en effet pas nécessaire tant que tu souhaites donner accès au constructeur Reader. Par exemple en haskell, tu peux écrire getNamePart = Reader $ \p -> concat ["Bonjour je m'appelle ", name p]
  • il me semble que le runReader du bout de code haskell s'appelle "read" dans le bout de code scala, non ?

en tout cas c'est bien cool de voir ça, merci !

@julienrf
Copy link
Author

julienrf commented Dec 9, 2011

Oui oui, tu as raison pour tes deux points :)

(en fait, dans une première version j’avais omis la méthode read mais finalement je l’ai ajoutée parce que je pense que c’est plus lisible)

@md2perpe
Copy link

md2perpe commented Dec 9, 2011

newtype Reader env res = Reader (env -> res)

runReader :: Reader env res -> env -> res
runReader (Reader f) e = f e

newtype Reader env res = Reader { runReader :: env -> res }

@paul-r-ml
Copy link

md2perpe : thank you for this comment, this is indeed a correct alternative implementation, and shorter. But I really dislike this usage of records syntax for containers, because it reads poorly in my opinion, leading to unintuitive code. For example, I'd be interested to ask julienrf, who is not used to haskell community tricks, which of these two implementation is easier to follow.

@julienrf
Copy link
Author

I’m a bit surprised because I didn’t know that syntax, I thought the :: were only used to write type anotations, but besides that I think it’s ok. Actually it made me realize that I could use the same trick in Scala: I’ve modified the Reader class to copy md2perpe’s style :)

@md2perpe
Copy link

@paul-r-ml: But you use this record syntax yourself in the Person type.

@paul-r-ml
Copy link

@md2perpe : indeed, but I try to only use it for product types (tuple types), for the sake of naming constructor fields. In my opinion, this is the original design for record syntax. A person obviously is composed of a 'name' and an 'age'. But a reader is not obviously composed of a 'runReader' :)
Again, that's really a matter of taste.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment