Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make the Lazy basic implementation much faster... #2

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
1 change: 0 additions & 1 deletion elm-package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{
"version": "2.0.0",
"summary": "Basic primitives for working with laziness",
Copy link

@mgold mgold Feb 1, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please revert this. Version bumps are part of the release process, not the pull request process.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, didn't realize.

Reverted.

"repository": "http://github.com/elm-lang/lazy.git",
"license": "BSD3",
Expand Down
77 changes: 51 additions & 26 deletions src/Lazy.elm
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
module Lazy exposing
( Lazy
, force, lazy
, force, lazy, lazyFromValue
, map, map2, map3, map4, map5
, apply, andThen
)

{-| This library lets you delay a computation until later.

# Basics
@docs Lazy, lazy, force
@docs Lazy, lazy, lazyFromValue, force

# Mapping
@docs map, map2, map3, map4, map5
Expand All @@ -25,8 +25,9 @@ import Native.Lazy


{-| A wrapper around a value that will be lazily evaluated. -}
type Lazy a =
Lazy (() -> a)
type Lazy a
= Evaluated a
| Unevaluated (() -> a)


{-| Delay the evaluation of a value until later. For example, maybe we will
Expand All @@ -41,7 +42,25 @@ Now we only pay for `lazySum` if we actually need it.
-}
lazy : (() -> a) -> Lazy a
lazy thunk =
Lazy (Native.Lazy.memoize thunk)
Unevaluated thunk


{-| `lazyFromValue' Sets the created Lazy a to an already evaluated value.
For example, maybe we want to set the tail of a lazy list to an Empty node so
there is no need to defer the calculation as it is a simple constant:

type LazyList a = Empty | Cons a (Lazy (LazyList a))

shortLazyList : LazyList Int
shortLazyList =
Cons 1 <| lazyFromValue Empty

Now the tail of the shortLazyList is immediately available to
force without calling an evaluation functtion.
-}
lazyFromValue : a -> Lazy a
lazyFromValue v =
Evaluated v


{-| Force the evaluation of a lazy value. This means we only pay for the
Expand All @@ -61,8 +80,10 @@ the first one, but all the rest are very cheap, basically just looking up a
value in memory.
-}
force : Lazy a -> a
force (Lazy thunk) =
thunk ()
force lzy =
case lzy of
Evaluated a -> a
Unevaluated _ -> Native.Lazy.memoize lzy



Expand Down Expand Up @@ -134,30 +155,34 @@ apply f x =
steps that all need to be performed lazily. This can be nice when you need to
pattern match on a value, for example, when appending lazy lists:

type List a = Empty | Node a (Lazy (List a))
type LazyList a = Empty | Cons a (Lazy (LazyList a))

cons : a -> Lazy (List a) -> Lazy (List a)
cons first rest =
Lazy.map (Node first) rest
cons : a -> Lazy (LazyList a) -> Lazy (LazyList a)
cons v lazylist =
Lazy.map (\x -> Cons v <| lazy (\() -> x)) lazylist

append : Lazy (List a) -> Lazy (List a) -> Lazy (List a)
append lazyList1 lazyList2 =
append : Lazy (LazyList a) -> Lazy (LazyList a) -> Lazy (LazyList a)
append list1 list2 =
let
appendHelp list1 =
case list1 of
Empty ->
lazyList2

Node first rest ->
cons first (append rest list2))
in
lazyList1
|> Lazy.andThen appendHelp


By using `andThen` we ensure that neither `lazyList1` or `lazyList2` are forced
appendi lazylist1 =
let appendHelp ll1 =
case ll1 of
Empty -> list2
Cons first rest ->
cons first (appendi rest)
in
lazylist1 |> Lazy.andThen appendHelp
in appendi list1


By using `andThen` we ensure that neither `lazyList1` nor `lazyList2` are forced
before they are needed. So as written, the `append` function delays the pattern
matching until later.
Note that although this is the way to write `cons' and `append' for a `Lazy (Lazylist a)`
so as to avoid "The Halting Problem" and make infinite lazy lists possible inside the wrapper,
which would otherwise cause stack overflow (or detection and an exception thrown),
the extra level of laziness of the outer `Lazy' wrapper costs much in terms of performance
due to the number of force/thunk/lazy chains of function calls/composition needed.
-}
andThen : (a -> Lazy b) -> Lazy a -> Lazy b
andThen callback a =
Expand Down
20 changes: 9 additions & 11 deletions src/Native/Lazy.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
var _elm_lang$lazy$Native_Lazy = function() {

function memoize(thunk)
{
var value;
var isForced = false;
return function(tuple0) {
if (!isForced) {
value = thunk(tuple0);
isForced = true;
}
return value;
};
// mutates `lzy` Unevaluated thunk into Evaluated value, returning value.
function memoize(lzy) {
if (lzy.ctor === 'Evaluating')
throw Error("Lazy.memoize: recursive evaluation error!!!");
lzy.ctor = 'Evaluating';
var v = lzy._0(lzy); // dummy placeholder arg
lzy.ctor = 'Evaluated';
lzy._0 = v;
return v;
}

return {
Expand Down