Saw this on Haskell Weekly: How a Haskell programmer wrote a tris in PureScript. The description said, “Purescript has tried to do things ‘better’ than Haskell but they fall short of it in several ways that ends up being infuriating.”
I ignored the implementation the author explained and read the critiques presented (quoted below). My question to the community is, “Which critiques are valid points that should be considered and which are ‘noise?’”
Right after I finished the work I felt like shitting bricks. Purescript has tried to do things “better” than Haskell but they fall short of it in several ways that ends up being infuriating.
Purescript issues
The first thing that caught me off guard is Purescript’s lack of tuples. They got a “Data.Tuple”, but there’s no infix syntax for declaring tuples. I ended up rewriting everything in records and that resulted in pages-long type errors that required me to scroll upwards while Purescript’s emitted warnings about implicitly imported variables before them.
Also, what’s the deal with number literals? In Haskell the type signatures for number literals 3
and 3.0
are Num t => t
and Fractional t => t
. In purescript they are Int
and Number
… I continue and try this with other things.
> :t 3
Int
> :t 3.0
Number
> :t "foo"
String
> :t [1,2]
Array Int
You’re serious about this? Overloadable literals are some of the best stuff that Haskell has to offer and you did not implement them into this language? Come on! Why would anybody want to use it in this way?!!
> :t (+)
forall a. Semiring a => a -> a -> a
At least they got…
Semiring Number
I feel pissed. When you write “Semiring”, it means it must satisfy certain rules for semirings. Floating point addition is not associative. Why did you do this when you didn’t abstract the literals? This is translating to Javascript’s double right? “What Every Computer Scientist Should Know About Floating-Point Arithmetic”, just wow.
The another thing I really don’t like is how much stuff is off the Prelude. Look at all this stuff I had to import into my program.
module Main where
import Data.Array ((..), index)
import Data.Either (Either(..))
import Data.Foldable
import Data.FoldableWithIndex
import Data.Int (toNumber)
import Data.Map (Map, insert, union, intersection, keys, empty)
import Data.Maybe (Maybe(..))
import Data.Number.Format (toString)
import Data.Set (size, filter)
import Data.Traversable (sequence, sequence_)
import Effect.Aff (Aff, delay, Milliseconds(..), launchAff_)
import Effect.Class (liftEffect)
import Effect.Console (log)
import Effect (Effect)
import Effect.Random (randomInt)
import Effect.Ref (Ref, new, read, write)
import Graphics.Canvas
import Prelude
import Web.DOM.Document (toNonElementParentNode)
import Web.DOM.Element (toEventTarget)
import Web.DOM.NonElementParentNode
import Web.Event.Event
import Web.Event.EventTarget
import Web.HTML.HTMLDocument (toDocument)
import Web.HTML (window)
import Web.HTML.Window (Window, document, requestAnimationFrame)
import Web.UIEvent.KeyboardEvent
How come I need to import arrays, number formatting, foldables, traversables, either or maybe? Why the rest of the stuff is divided into so many modules? There are like more than 5 modules for accessing HTML elements. Also I’m a bit annoyed when I see things like this:
addEventListener :: EventType -> EventListener -> Boolean -> EventTarget -> Effect Unit
So… I need to convert things into EventTarget when I use this function. Also how come EventType doesn’t determine the EventListener’s shape? I got some slight Elm vibes from this. At least there are typeclasses but why does this seem otherwise so conservative when it comes to type level stuff?
There’s also a thing that I don’t like at all about in Purescript. It’s the foreign function interface. This is how you expose stuff to Purescript. First you write a module in Javascript:
exports.diagonal = function(w) {
return function(h) {
return Math.sqrt(w * w + h * h);
};
};
Then you describe this for the Purescript:
foreign import diagonal :: Number -> Number -> Number
Why are the types Purescript when it’s Javascript on the other side? What I suspect is happening is that “imports” are doing nothing here. It’s just exposing how purescript compiles to javascript. It might happen like this, for instance:
codegen :: Term env type -> Javascript
codegen (Var index) = lookup index
codegen (Lambda fn) = do
(arg, body) <- abstract (codegen fn)
pure ("function(" <> arg <> "){" <> block body <> "}")
codegen (App f x)
= codegen f <> "(" <> codegen x <> ")"
This is what you might like to do if you want to get some abstract machine, such as Javascript, to evaluate lambda calculus. It’s inevitable that the implementation produces some type there but why is it exposed? Also is that why there are record types with row polymorphism? Did you do the dumbest thing you can do there and copy the implementation details into the syntax of your language? It seems a lot like it:
Purescript | Javascript
-----------+-----------
Boolean | Boolean
String | String
Int,Number | Number
Array | Array
Record | Object
There’s an additional layer that’d belong here you know…
- Javascript’s values are just valid structures that your language can carry around. There’s no need to pretend that you couldn’t pass plain untyped Javascript values as such.
- Your source language’s types should not mix with Javascript’s types. They are separate entities and you aren’t supposed to reveal how they’re implemented. It varies by how the language is implemented.
- You can construct structures that relate javascript values into your language’s types. It would be much more pleasant to use.
This isn’t too hard to do if you’re building a new language. For instance, you could do it all through a separate module that you import to declare things.
import FFI.Javascript (Function, Double, Inline)
diagonal :: Number -> Number -> Number
foreign diagonal (Function [Double, Double] Double)
(Inline "function(x,y){ return Math.sqrt(x*x + y*y); }")
Also don’t expect that we’d want only one implementation!
import FFI.C (Function, Double)
import FFI.C.So (SharedObject, Win32, Linux)
foreign diagonal (Function [Double, Double] Double)
SharedObject
[Linux "libDiagonal.so", Win32 "libDiagonal.dll"]
"diagonal"
When it comes to typed languages it would be almost like I am the only one who understands this: The datatypes presented by the language do not need to correspond to hardware datatypes or implementation datatypes. They also do not need to stay same representation during the code generation step.
Overall the experience of Purescript is not bad enough that I’d have stopped and forgotten about this whole thing. That’s already an achievement.