1

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.

Functor
  • 582
  • 1
  • 9

2 Answers2

6

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
Vikstapolis
  • 744
  • 3
  • 13
  • Thank you very much for your thorough explanation which is very clear to me/us and extremely informative!! – Functor Feb 05 '22 at 07:04
  • 1
    "you need to fully evaluate the inside of x for the side effect to take place" is not really right. I don't know what it means to evaluate the inside of x, but the way to make an IO effect take place is to make it a part of the special IO action `main`. Even your `f` will not print anything unless it is included in `main`: `const 10 f` evaluates to 10 and prints nothing. – amalloy Feb 05 '22 at 07:57
  • @amalloy You're right, thanks for the correction. I'll edit my answer – Vikstapolis Feb 05 '22 at 08:08
  • "The only way to print output, i.e. have an impure function" I don't think this is correct. To my understanding there are no impure functions. And functions returning an `IO a` are no different from that, they are pure, nothing about `IO` is special. It is only when you execute your program that values of type `IO a` cause an effect on the machine you execute it on. – Micha Wiedenmann Feb 05 '22 at 13:05
  • @MichaWiedenmann while you may be technically correct, an ideal description of Haskell IO was not the goal of this answer, and adding such details would only complicate it and distract from the actual question. It is simpler to refer to computations within `IO` as impure, and it's also true that `IO` is not an ordinary type. – Vikstapolis Feb 05 '22 at 13:54
  • @Vikstapolis How is `IO` not an ordinary type? – Micha Wiedenmann Feb 06 '22 at 07:54
  • @MichaWiedenmann Saying that `IO` is just an ordinary type can be misleading, because 1.IO isn't referentially transparent; 2.It allows side effects (which is in the name really); 3.When programming in `IO`, exception handling, lazy streaming etc. need to be considered;. This is what my definition of impure is. Stating that IO is only impure when a program is executed is pointless, because all programs are made for the sole purpose of being executed. Already, this has strayed from the OP's question, and unnecessarily complicates the answer, which is why I didn't mention any of this in it – Vikstapolis Feb 06 '22 at 09:05
  • Also, the question was about a translation from JS to Haskell, which was not possible in this case - IO in Haskell is very different from IO in most other languages, and the topic of purity is necessary to explain this difference – Vikstapolis Feb 06 '22 at 09:07
4

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.
amalloy
  • 89,153
  • 8
  • 140
  • 205