Haskell and OCaml are both brilliant languages, with excellent feature sets and amazing dev teams... and lots of "engineers" who like them.
Kototama: ...I have the feeling that OCaml may be better for engineers.
You: let's not say that OCaml is better for engineers.
People are allowed to feel things.
But I'll give you a concrete reason why OCaml actually can be better for engineers: named and optional arguments. This single feature does wonders for code readability and maintainability.
Actually I'll throw in a bonus reason: no laziness by default i.e. small and simple, predictable runtime that behaves almost exactly like a C executable.
But yes. Optional arguments are a big problem at scale. And laziness is the worst antifeature I could imagine.
But. You can disable laziness and get great performance. You just need to be careful when using libraries and pick ones that have the right strictness.
A strict Haskell would be a killer language.
Honestly a deal breaker
> A strict Haskell would be a killer language.
PureScript may be this language!
I’m someone who isn’t an experienced functional programmer, or that deeply knowledgeable about type systems and category theory etc, but OCaml is still approachable to me, I feel I can get stuff done in it. I didn’t feel that way with Haskell - I tried, but found the number of ways to do things overwhelming, and a lot of it felt very theoretical. Of course, Haskell’s pureness is an impressive and interesting feature; but it isn’t for me.
OCaml also has much better systems programming abilities compared with Haskell, IMO. It feels like OCaml is the Rust of the functional programming world (and this is no accident, the Rust compiler was first implemented in OCaml in fact).
This is all completely subjective of course.
Where OCaml wins is that it does not hate mutation. This makes the code more impure but simpler. Sometimes just incrementing a mutable variable makes more sense than introducing a state monad!
OCaml's variables are immutable. To use mutation, you have to do so explicitly with the `ref` type, e.g.:
let box = ref 0 in
print_endline ("box value: " ++ int_of_string !box);
box := !box + 1;
print_endline ("box value: " ++ int_of_string !box)
will print first `0` and then `1`. You cannot put a non-integer into the box because the box has type `int ref`, meaning only values of type `int` can be put into it with the `:=` operator.For example in normal Haskell the code that uses mutation and code that does not use mutation have different types. A function may have the type
f :: Int -> Int
but with mutation it can become f :: Int -> ST s Int
or f :: Int -> IO Int
or f :: Int -> STM Int
depending on the circumstances. That's not the case in OCaml. The kind of mutation in OCaml references is the most similar to Haskell's ST monad (not to be confused with State or StateT), but in Haskell the type tells us a mutation is happening inside the function call.Adding mutability to the mix via IORefs + Haskell's laziness by default means you don't _really_ know when a particular IORef mutation may really be executed.
In OCaml, evaluation is eager and you know when it will happen. Even when you introduce bangs and special Haskell language extensions, fully removing laziness from your code in Haskell is difficult.
TL;DR: Because of Haskell's laziness you are in some ways forced to be purely functional. OCaml allows you to embrace mutation in ways Haskell does not, in my view.
> But in a multi-core environment you will eventually want a guaranteed STM.
You actually may not always always want STMs. STMs are awesome but something more basic like locks may do the job. It can be more performant to use, say, a mutex. If STMs were superior on every parameter they would have taken over the world. They have not -- they are good in some scenarios and not so good in others...
Why would one prefer to fully remove laziness?
> TL;DR: Because of Haskell's laziness you are in some ways forced to be purely functional. OCaml allows you to embrace mutation in ways Haskell does not, in my view.
unless there's a strive to be more on the side of the Safe[1] language subset, Haskell can be told to embrace the same mutation property with a combination of `unsafePerformIO` and `seq`. People tend to avoid it because the language offers a tooling to achieve better mutation properties. Overall, my argument here is that OCaml's idiomatic approach to permissible mutations doesn't seem to be a competitive advantage when it comes to software development in general, but rather a flavour that imperative programmers prefer due to their familiarity with the concept.
> You actually may not always always want STMs.
Whenever there's a non-trivial retry policy, one should prefer to choose STM + queues over debugging adventures with Locks and Mutexes. Almost all of multithreaded software I can immediately think of have non-trivial retrying logic.
[1] https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/safe...
Far from an Ocaml/Haskell expert, but is it that laziness makes resource use less predictable?
[1] https://wiki.haskell.org/GHC_optimisations
[2] https://markkarpov.com/tutorial/ghc-optimization-and-fusion....
https://medium.com/@asankhaya/reason-with-ocaml-b75697a6c00b
That said I don't know Haskell to compare!
Well I don't think you should have been the one embarrassed in that scenario.
I don’t think Ocaml had that backtick infix syntax for non-symbol function names though!
OCaml offers a pragmatic functional approach to programming. And now you are going to be able to have your OCaml code run in a truly parallel fashion on your multicore CPU.
In the future there are plans to add typing to effects though. (There is support for effects currently but its untyped and experimental). When that happens you can track changes to state (which is a kind of "effect") if you want to...
that wasn't the OP's argument though, the argument was that OCaml is somehow generally better for engineers.
> OCaml offers a pragmatic functional approach to programming.
an evaluation of something as pragmatic depends purely on what one whishes to practice. There's no universally objective notion of pragmatism.
> And now you are going to be able to have your OCaml code run in a truly parallel fashion on your multicore CPU.
no, it won't be able to do that automatically. Your code will have to respect certain invariants to function properly, and you as a developer will have to enforce these invariants with the available tooling at hand. Haskell has purity, guaranteed STM, and `par` labels for that. OCaml doesn't have those and the existing codebases will have to eliminate their thread-unsafe public interfaces first.
> When that happens you can track changes to state
how are you planning to track state changes without purity?
> no, it won't be able to do that automatically. [...]
I was comparing it to the old situation in OCaml is that it was impossible to have threads that were executing pure OCaml code and were _not_ IO bound to execute truly parallely. That limitation is removed.
To me it is pretty obvious that you will need to use things like atomics, thread safe data-structures, mutexes etc. to ensure your code runs properly in multicore OCaml. It was implicit in my response. But I should have been more explicit.
> that wasn't the OP's argument though, the argument was that the OCaml is somehow generally better for engineers.
Engineers tend to be pragmatic. OCaml is pragmatic. So in quick short form, OCaml may be better to solve a certain kind of problem than something that is more pure and abstract like Haskell. It was intended as an informal argument and not an argument in a court of law :-).
Like math? Engineers would rather not use math?
> It was intended as an informal argument
Maybe. But at this point it just looks like a mean dig at Haskell. A better word would be opinionated. Haskell is opinionated, and that is fine.
The biggest bait in this thread is using the term "engineers". It should rather be: people trained in imperative programming in mainstream imperative languages. Then it makes sense.
Gosh yes engineers would rather not use math. If there are approximations that work well enough (and when incorrect goes the way that leads to buildings not falling down) engineers definitely like to use less math.
For one thing, less math means less room to make mistakes, and often ugly but works well is better than elegant with a higher chance of failing due to people screwing up.
And approximations are not math all of the sudden?
> definitely like to use less math
Is your point that they would rather work less than more? Easier than harder? Cus that is not very relevant and applies to every kind of work.
Or less math and more something else that you hand wave so hard it is not clear what it might be?
> And approximations are not math all of the sudden?
The context was Haskell versus OCaml, so yeah — Haskell is the more academic, pure math. Ocaml (in this context) is the more approximate, but more practical option for larger projects. In practice people use things like C and Python because spherical cows are close enough.
It is especially funny in the thread about OCaml getting soon (2022 maybe) multicore support. Something this less practical ivory tower Haskell had before it was even cool.
Maybe, there are better options out there these days for parallel and concurrent programs. Parallel programming in C and C++ is extremely fraught for the very reasons the parent brings up. There are so many footguns, starting from the fact that the favorite debugging method of C programmers, printf(), is not even thread safe.
It’s so bad that Rust markets as a feature “fearless concurrency”, capitalizing on the recognition that the prevailing emotional state of a C or C++ dev writing concurrent or parallel programs is one of fear.
And the very thing that makes Rust concurrency fearless over C and C++ is that borrowing and mutability are explicitly tracked. As we enter a world where over a dozen CPU cores are the norm, we are learning what works and what doesn’t in writing programs for these machines, and integrating those learnings into new languages.
Most everyone I know uses a debugger. Maybe you're mostly exposed to beginner programmers?
So you end up with legions of Linus hatting on debuggers and proud themselves of never having to use one, some kind of macho thing I guess.
The last point may not be obvious, but debuggers have tons of noise for complex programs. In practice I just want to see how my program state changes over time while a debugger shows the entire program state or a large subset of it.
I think the future of debugging is going to be structured program state logging. Ideally we should be able to take our logs and partially reconstruct program state over time. For example, in addition to source location, we should save the lexical information for each variable logged so you can have interactivity with your logs and source code.
Which in the age of micro-services is quite relevant as well.
Definitely better than other languages, still doesn't prevent one to actually think about end-to-end overall system design.
It basically requires eager evaluation which, believe it or not, is a huge ergonomic win for people wanting to work on practical problems.
OCaml also preserves the concise syntax and great type inference you get in Haskell.
All in all, a more pragmatic set of tradeoffs I'd say, although it certainly has its own problems.
What is untracked? Is
unsafePerformIO (printLn ...)
tracked? What is tracked? Is foo :: IO ()
thread-safe? Maybe, maybe not. What meaningful information does this signature says to me? That it does some IO? That's an extremely useless information, especially if most of your code is IO something.What granularity does IO have? Does
foo :: IO ()
throws any exceptions? Maybe, maybe not.The need for effect tracking for writing correct programs is way overstated by some Haskell programmers. It's usually much more prudent to write DSLs which hide effects like variable mutations and logging, than expose them.
For example you can start your program with a pure correct-by-construction core DSL, and then add logging, mutable variables where needed underneath the DSL's terms without breaking the semantics of the DSL. With effect tracking you are doomed to either reinvent custom effects to be able to switch interpreters painlessly, or you'll need to break the DSL by the addition of effects.
Neither is prudent in real world, neither gives more value than takes. What is really funny is that some Haskell programmed believe that logging should be tracked, but allocation apparently should not (yes, it's a side effect).
Where in fact all the "effects" are need to be tracked only when they are meaningful, i.e. when they are part of our DSL's semantics, and not some hidden part of the interpreter I don't need to know about.
Imagine a theorem prover function which creates a conjunction of two terms:
val conj : term -> term -> term
It doesn't matter if it allocates or logs, at the precision that is interesting to us, it's just a function creating a conjunction of two terms.This repetitive talking point is getting boring. Go figure whether it's tracked now:
{-# LANGUAGE Safe #-}
-distrust-all-packages
> What meaningful information does this signature says to me? That it does some IO? That's an extremely useless information, especially if most of your code is IO something.That's an extremely narrow view which you wouldn't have if you ever tried to implement a safe sandboxed environment.
> What granularity does IO have? Does [...] throws any exceptions? Maybe, maybe not.
what does it have to do with tracked parallel mutations?
> Neither is prudent in real world, neither gives more value than takes
define prudent and define real-world.
> It doesn't matter if it allocates or logs
have you heard about referential transparency? It's the thing your "theorem prover" example does not have.
That is, an environment without effects? In Haskell you could't do so, since pure functions allocate, throw exceptions, trigger GC etc.
> have you heard about referential transparency? It's the thing your "theorem prover" example does not have.
Of course it's.
conj "A" "B"
can be safely replaced by "A /\ B"