What are the community's thoughts on a Haskeller's critique of PureScript?

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…

  1. 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.
  2. 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.
  3. 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.

2 Likes

Sounds like since they are the only one who understands how it should be done, they should write their own language.

8 Likes

which…no surprise to me…he seems to have done…classic autodidact. i couldn’t be arsed looking thru it for any valid criticisms myself.

They present valid points, but there are also a lot of what I believe to be invalid points. Here’s a PureScripter’s critique of “a Haskeller’s critique of PureScript”:

  1. PureScript’s lack of tuples: Seems like they don’t know about /\.

  2. No polymorphic number literals: PureScript doesn’t have Haskell’s Num and Fractional. I wonder how they would work in PureScript. i don’t even know if it’s possible to integrate it with PureScript’s granular typeclass hierarchy in a natural way. In any case, I never really wanted them that often. I believe adding them now will also break a lot of code.

  3. Number addition is non-associative; thus it is not a valid member of Semiring: They present a valid point here. However, making a separate plus operator for Number would force us to duplicate functions: one for Semirings, and one for Numbers. Also, Int is not a valid Semiring due to using floating-point multiplication under the hood. Thus, we’ll need to duplicate each function three times. This is one of the rare places where PureScript opted for convenience instead of purity.

  4. Too small prelude: PureScript was designed to allow alternative preludes. It isn’t at all difficult to create a FatPrelude that re-exports modules that are commonly used in your project.

  5. The addEventListener thing: I don’t really understand what they’re saying here, so I’m just gonna skip it.

  6. The FFI: I don’t see how this exposes the implementation details of the PureScript compiler. This FFI is the same with the Python backend, the Erlang backend, and all other backends. Row types and row polymorphism are very useful, and the practicality of the language would be severely limited without them. Also, do you even need a reason to implement row types? They’re too awesome to not implement.
    In addition, is there anything wrong with the fact that PureScript types correspond directly to JavaScript types? These types are also agnostic to the target language. They work with Python, Erlang, Go, etc.

  7. The alternative FFI: I’d rather not write the JavaScript code in PureScript. Imagine how purescript-aff would come out! (Shudders) Also, this design ties the foreign functions to one backend, which is A Bad Thing :tm:.

I’m sure there are other stuff that I didn’t say, but writing all this tired me out, so I think this is enough.

3 Likes

As it happens overflow doesn’t actually make Int an invalid Semiring instance. Int can be a valid semiring (in fact, a valid ring), because it is equivalent to the ring Z/(2^32)Z, integers mod 2^32. The current implementation is not valid for a different reason, which is that it uses floating point multiplication, which can give incorrect results when the product of the result is larger than 2^53 before it is truncated to fit within the Int range. This is fixable though: https://github.com/purescript/purescript/issues/2980

As for the actual post, yeah, the way I see it it’s just a polemic and it’s not very interesting to me. I skimmed it in enough detail to spot that the author doesn’t know what they’re talking about, and I don’t think I’m going to spend any more time thinking about it.

1 Like

Oh actually one more thing: I will go on record to say that I think overloaded literals are a misfeature and I’m very glad we don’t have them.

6 Likes

Ah, sorry for the mistake. I misread the Semiring docs. Will fix it right away.

By the way, my reply seems like a good addition to the PureScript FAQ. @hdgarrood are there any other inaccuracies in my reply?

No worries - the Semiring docs are definitely unclear on this point at the moment.

Everything else in your reply looks accurate to me, but I’d suggest being judicious about what you include; not every random polemic we found on the web merits a response in an FAQ. It’s sometimes easy to forget that the “F” stands for “frequently”, I think.

As it happens it used to be possible to write JavaScript FFI inline in PureScript source files; that was removed deliberately a really long time ago (I think in 0.7.0?) because it made alternative backends significantly more awkward to maintain.

4 Likes

Of course I won’t put everything from my reply. These are the questions I came up with so far:

  • Why is the prelude so minimal? I don’t want to import 10 modules in every file.
  • Why is there no built-in support for tuples?
  • Why doesn’t PureScript support polymorphic number literals? (Not sure if this is actually frequently asked)

I do remember reading about the inline FFI somewhere. I’m glad it was removed.

5 Likes

Thanks @mhmdanas and @hdgarrood for providing a concise response to the critiques. I’m glad it led to something productive (i.e. a few more questions for the FAQ) and provides documentation in case someone asks similar questions in the future.

6 Likes
  • Part of the rationale for the lack of innate tuples is because records can often be used instead.
  • I’d be happy to have overloaded literals personally, but as per Harry’s response it’s not an obvious/uncontroversial feature.
  • EventType cannot determine the type of EventListener because JavaScript. Event is only ever going to be the safe type here, as there’s no guarantee that a click is raised by the DOM - someone can call dispatchEvent with “click” as the type but something other than MouseEvent, for example.
  • The DOM/HTML API being split into many modules, etc. criticism is understandable, but it’s just implementing the actual API without trying to be opinionated about it. It’s often complained about but nobody has made a higher level thing yet, so :man_shrugging:
  • The original inline FFI was painful to deal with. It invariably involved writing JS elsewhere first, then pasting it in. At least you can use existing lint tooling, syntax highlighting, etc. with the current arrangement.
  • I’m not sure I understand the criticism of using row polymorphism for records. Why wouldn’t you?
  • I don’t really follow the 3 points about JS vs language types, it sounds like a description of what we do?
4 Likes

The DOM/HTML API being split into many modules, etc. criticism is understandable, but it’s just implementing the actual API without trying to be opinionated about it. It’s often complained about but nobody has made a higher level thing yet, so :man_shrugging:

I’m curious what a higher-level api might look like… Any ideas?

1 Like

I honestly don’t have much of an idea, but people often seem to think it’s being done wrong at the moment. :smile:

There was dom-classy that I maintained for a while in the past, which just added classes for casting and coercion and had methods constrained by class rather than type for the elements/nodes/etc, but that was more necessary because originally the library only provided “one level” of casts and coercions for each type, so sometimes several would need chaining together when writing them manually. It was also annoying to use dom-classy though, since it could be difficult to figure out the problem with some of the errors it raised. And it made using ?holes more difficult, as with any class-heavy interface.

1 Like

Hi, it’s the author of the post here. Thanks for the holier-than-thou welcome toward all autodidacts, yes we shall ignore your argumentation to favor our attained status and much of apparent education that doesn’t show up in elsewhere than CV (the thesis work and the education you received is what matters), I see I have found my way to a great company that matches my level in annoying people off.

I didn’t put much argumentation behind my “purescript issues”, perhaps I should have as it would have helped. If you get to become bothered by this, please look toward argumentation, against and in favor. I could very well be in wrong when writing this kind of bursts and my opinions are subject to change.

The FFI is probably a hard one to get correct, and when pissing over how the totality/partiality checking is implemented I acknowledge it is pretty difficult to even evaluate and maybe I’ll look at that one closer, though I do almost hate zero parameter constraints and haven’t entirely made up my mind of that yet. Actually it may turn out to be even brilliant, but then you’re not checking termination and only catch partial case patterns.

The utilities to manipulate dom from purescript was neat in itself. Whatever is being done, I hope it’s resolved somehow that older stuff doesn’t break. I’m tired of fixing stuff after npm changes C-API for the sixth time in a week. I didn’t write about it but I’m actually worrying about that a bit. Did you learn every lesson that cabal/nix had before building up spago?

The literal typeclassing is the real mystery here. Are you really trying to paint it as an “misfeature”? Technically there would be a bit of problem to include it now, because Int doesn’t cover every literal, neither the Number does that for every fraction, if it’s just a double renamed. Then there’s a question of having an inlining or normalization before compiling that feature might need to come out as convenient. I’d suppose you have that though?

The implementation isn’t a problem, you just (fromIntegral n) things instead of dropping the literal in directly. Gain of this is that you get multiple interpretations for numerical values. It’s useful for computer algebra in general so I’d favor not leaving it out. It’d be even neater with your non-semiring semirings than what it is in Haskell.

If you got any questions I’ll be answering, but next going to focus on studying more things out. I likely won’t touch Purescript in any way for few months or so. Still, there’s already more project ideas coming up. I’m definitely trying this out for a second time.

If you get to become bothered by this, please look toward argumentation, against and in favor.

This is not how we do things here. Tone and psychological safety matter; I don’t want a community which is happy to overlook insults just because there might be some useful information buried inside them.

19 Likes

I find some tolerance toward what other people do say, would be advisable. You know how it feels to the outside? It feels like walking on eggshells. It’s very taxing in itself.

It also turns out asymmetrical. Like… Somebody inside a community has a right to hit, but outsider is not allowed to hit back.

But it’s ok either way. I let you guys be, ok?

It’s extremely taxing as a maintainer to read and respond to content like this. I just don’t want my brain filled with this kind of stuff for a whole day. Why go through the effort to write in this tone and publish it? To those sharing, why share it? I personally don’t have patience for it.

I don’t mind discussing the finer points, but I just don’t want to wade through the tone to find something worth responding to. I’d recommend that if someone wants genuine discussion on this stuff, then they can distill down points. Otherwise I say just lock it and move on.

10 Likes

I’m trying things out and vented out my frustrations while at it. If you look at a ramble toward things it may entertain. But BLAMMO it immediately reached the person who actually made the thing. I have deep respect for whoever worked on Purescript.

There’s tons of things and it’s probably better to look at them one-by-one and at a better time. The first and perhaps the most interesting thing is the lack of overloaded literals. I can collect up something to reason why they should be in a language. There’s one thing that helps me to build a case though.

I’d be really interested about why are you thinking overloaded literals are a misfeature? I’ll look in at the new posts, faq, etc. I’ll try to find out about this myself so no need to reiterate if you’ve already said it and it can be found.

1 Like

I appreciate seeing critiques, even if the tone is antagonistic. Others may have had similar bad experiences with the language, but simply move-on without sharing feedback. Many of the critiques are based on false assumptions, but that’s still something we can address with improved docs.

To touch on a few additional points:

We have IDE support to automatically clean-up implicit imports. Maybe we’re due for an “IDE tips” guide.

Random selection from NonEmpty is a more robust option. This would probably be easier to discover outside of quickcheck.

I agree most with this point, and a lot of us are working on making this better.

Please don’t lock the thread. I’d like to eventually follow-up with a tris rewrite. There have been requests for more examples of games, and this could be a nice blend of Monadic Adventures and Multipac.

2 Likes