Home > Enterprise >  Sequential ordering of evaluation in Haskell
Sequential ordering of evaluation in Haskell

Time:02-05

To obtain a sequential evaluation in a functional context, in JavaScript, I frequently use

const right = a => b => b;

const f = () => right(console.log("hi"))(true);

console.log(f());

This is somewhat similar to do thread and much simpler to implement but only works in eager-evaluation languages such as JavaScript.

In Haskell, since the language works in a lazy-evaluation/call-by-need manner, the argument of a will not be evaluated because it's not required in the lambda expression body.

So, I wonder what would be the simple smart approach to implement the right function that works in the lazy evaluation. <- This is my #1 Question here.

There are Haskell wiki articles:

Sequential ordering of evaluation

The seq primitive, despite its name, is not required to evaluate its parameters in some predefined order, according to the Haskell 2010 Report. For parallel programming, sometimes a variant of seq is needed with such an ordering:

While we focus mainly on the implementation, our work has had some impact on the programming model: we identify the need for pseq as well as seq (Section 2.1), and we isolated a signficant difficulty in the "strategies" approach to writing parallel programs (Section 7). Runtime Support for Multicore Haskell; Simon Marlow, Simon Peyton Jones and Satnam Singh. Instead of two or more similar primitives with similar purposes, extend the existing seq primitive with the traditional ordering of evaluation for its parameters - first, then second; with the role of simple strictness-control being performed by a new primitive with a more appropriate name e.g. amid, in Strictness without ordering, or confusion.

and

Strictness without ordering, or confusion

As the Haskell 2010 Report does not specify any order of evaluation with respect to its parameters, the name of the primitive seq :: a -> b -> b is a misnomer.

Introduce the primitive amid, with the same (Haskell 2010 Report) requirements:

     infixr 0 `amid`
     primtive amid :: a -> b -> b

     infixr 0 $!
     ($!) :: (a -> b) -> a -> b
     f $! x =  x `amid` f x

Actually, the function is exactly the same as my right; however, what I don't understand is as I mentioned in lazy evaluation, since a is not required in the function body, it shall not be evaluated. So what happens here? #2 Question

A code I think is

right = \a -> \b -> (const a . const b) 0

but, I don't know this one stably works.

CodePudding user response:

A major difference is that Haskell is pure, while JavaScript is not. In Haskell, if the result of a computation is never used, it makes no difference to the semantics of the program whether it is evaluated or not (ignoring runtime). This is because you cannot have side effects in a pure function, which includes printing things to the console, so if you don't use the function's result, it's as if you never called it.
The only way to print output, i.e. have an impure function, is if it is within the IO monad. So a function like this:

f :: Int
f = const 5 (putStrLn "hello")
-- const is just your `right` function with its arguments reversed

Cannot possibly print anything, since the function is pure (as indicated by the type signature).

Even if you use seq

f :: Int
f = let x = putStrLn "hello" in x `seq` const 5 x

You will not see anything printed - because seq will only force x to weak head normal form, i.e. only evaluate the IO constructor, but evaluation of an IO action does not imply its execution.

( EDIT: The only way to make an IO effect actually happen is for it to be in main, which is a special IO action. In the above definition of f, the expression evaluates to 5 :: Int and the putStrLn "hello" doesn't exist for any outside functions, so it can never be a part of main. Thanks to @amalloy for the correction. )

The only way to do so is by using the >>= function. However, the second argument of >>= needs to return an IO a. This implies that you can only force a side effect to take place and then return a value, if the value is returned in the IO monad.

In conclusion, monads are the only way to guarantee the sequential evaluation you are looking for:

f :: IO Int
f = do putStrLn "hello"
       return 5

Or equivalently,

f = putStrLn "hello" >> return 5

-- Can also be written as:
f = putStrLn "hello" >>= \_ -> return 5

CodePudding user response:

The fact that Haskell is lazy and pure has two results that are relevant to your question:

1. You cannot write this function

There is simply no facility in the language that allows you to say "evaluate this, then evaluate that". As you found seq is closest, in that it makes sure that its left argument is evaluated before it returns the right element. But it may evaluate b, then a, then return b if it likes.

2. You do not need this function

Evaluating an expression in Haskell never1 has any observable side effects. So flip const, equivalent to (\x y -> y), has the same behavior as this function you are imagining. It returns its second argument, and you can imagine that it evaluates the left one if you like, which has no effect because evaluating things never does. Your proposed implementation of right is buggy, in that it returns the left argument instead of the right one, but otherwise it has the same behavior again.

In the cases where you do need sequential ordering, it is not ordering of evaluation, but of the effects embedded in IO. Vikstapolis's answer covers this topic well already, so I will go into no further detail.


1 Okay, actually there are plenty of exceptions to this rule. But they are all cheating in some way or another, and do not really factor in here. Some of the most common exceptions:

  1. Evaluating an expression may allocate memory, or cause some currently-pending work to actually be done. If you need these effects, that's exactly what seq is for.
  2. Debug.Trace lets you write logs to the console as part of evaluating an otherwise-pure expression. This is only for debug use, so there is no standard-library function for fine-tuning the ordering: if you care about the logs an expression produces, you should really care about the expression itself too. The stuff in Debug.Trace is built on top of seq already, so if all left does is log something, you can just use a function from Debug.Trace, such as trace "hi" True.
  3. unsafePerformIO can perform arbitrary IO inside of supposedly-pure expressions. It is a tool reserved for very low-level stuff, shunned even by language experts except in dire need. No beginner should worry about this function.
  •  Tags:  
  • Related