-
-
Save julienrf/1448561 to your computer and use it in GitHub Desktop.
-- 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 f: Env => Res) { | |
def apply(env: Env) = f(env) | |
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))(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 “apply” du Reader construit par la fonction “presentation” | |
// Si j’avais appelé cette méthode “read”, j’aurais du écrire : “presentation.read(paul)” | |
println(presentation(paul)) | |
// Enfin, la notation monad-comprehension ! | |
// En fait c’est juste un sucre syntaxique pour: | |
// “getNamePart.flatMap(namePart => getAgePart.map(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[Person, String] { p => "Bonjour je m’appelle " + p.name } | |
def getAgePart = Reader[Person, String] { p => "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. » |
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)
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 }
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.
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 :)
@paul-r-ml: But you use this record syntax yourself in the Person type.
@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.
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 :
getNamePart = Reader $ \p -> concat ["Bonjour je m'appelle ", name p]
en tout cas c'est bien cool de voir ça, merci !