Playing around
Your question is fun and I haven't yet explored such a monad so I wanted to play around with it.
As Bergi points out, there's no need for the additional thunk wrapper in your data constructor – ie, Lazy
expects its argument to be a thunk already.
Your join
was broken – as counter-intuitive as it might seem, you have to make sure you wrap the unwrapping process! Think of it as adding one wrapper, but removing two; you still remove one layer of nesting, which achieves the intended result
Your monadic return
(or of
in this case) is also broken; return
is not always the same as your data constructor – ie Lazy.of(2)
should be equivalent to Lazy($ => 2)
Your code inspired me, so I tinkered with it until I wound up with this. I think you will be pleased ^_^ Bergi also suggested that Lazy
should not re-evaluated its result. I handled that with a safe memoisation inside the runLazy
method. Feedback on this would be appreciated <3
personal code style
As a convention, I write thunks as $ => expr
instead of () => expr
. When writing functional programs in JavaScript, you end up with a lot of ()
s, often times adjacent to other ()
s which can sometimes hurt readability. I think Lazy($ => f())
reads (at least) slightly better than Lazy(() => f())
. Since this is meant to be an educational site, I figure this is worth mentioning. I feel like small change helps with readability, but I also don't want to confuse anyone.
For anyone that's stuck, feel free to substitute ()
for all $
in the code below. Moving along now ...
// data Lazy = Lazy (Unit -> a)
const Lazy = t => ({
memo: undefined,
// runLazy :: Lazy a -> Unit -> a
runLazy () {
return this.memo === undefined
// console.log call just for demonstration purposes; remove as you wish
? (this.memo = t(), console.log('computed:', this.memo), this.memo)
: this.memo
},
// chain :: Lazy a -> (a -> Lazy b) -> Lazy b
chain (f) {
return Lazy($ => f(this.runLazy()).runLazy())
}
})
// Lazy.of :: a -> Lazy a
Lazy.of = x =>
Lazy($ => x)
// repeat :: Int -> (a -> a) -> a -> Lazy a
const repeat = n => f => x =>
n === 0
? Lazy.of(x)
: Lazy.of(f(x)).chain(repeat (n-1) (f))
// m :: Lazy Number
const m = repeat (5) (x => x * 2) (1)
console.log('computations pending...')
// ...
console.log(m.runLazy()) // (1 * 2 * 2 * 2 * 2 * 2) => 32
console.log(m.runLazy()) // => 32
As for satisfying the other categories, here's some more method implementations for Lazy
. I got hung up on the Monoid for empty
, but maybe you or someone else has some ideas there !
I also saw that you derived chain
from f => join(map(f))
which is perfectly fine too
Functor
// map :: Lazy a -> (a -> b) -> Lazy b
map (f) {
return Lazy($ => f(this.runLazy()))
}
Applicative
// apply :: (a -> b) -> a -> b
const apply = f => x => f (x)
// ap :: Lazy (a -> b) -> Lazy a -> Lazy b
ap (m) {
return Lazy($ => apply (this.runLazy()) (m.runLazy()))
}
Monad
// Lazy.of :: a -> Lazy a
Lazy.of = x =>
Lazy($ => x)
// chain :: Lazy a -> (a -> Lazy b) -> Lazy b
chain (f) {
return Lazy($ => f(this.runLazy()).runLazy())
}
// join :: Lazy (Lazy a) -> Lazy a
join () {
return Lazy($ => this.runLazy().runLazy())
}
Monoid
// empty
empty () {
// unsure on this one
}
// concat :: (Monoid a) => Lazy a -> Lazy a -> Lazy a
concat (m) {
return Lazy($ => this.runLazy().concat(m.runLazy()))
}
open exploration
This topic is very fun for me so I'm happy to discuss anything I've written about here or any other comments about the ideas presented in this question/answer. Let's have more fun!
join
should betx => Lazy(tx())
– Mulantx
= fancy type,ft
= function that returns a fancy type, fancy type = a type that represents a context with a value (the context ofLazy
is lazy evaluation and thunks respectively.() -> a
is the signature of a thunk, at least in Javascript) – user6445533