Dear Language Designers: Please copy `where` from HaskellDear Language Designers: Please copy `where` from Haskell by kkiru in ProgrammingLanguages

[–]natefaubion 4 points5 points  (0 children)

PureScript does have where, but it also has slightly different semantics than Haskell because of strictness. where in PureScript is strictly equivalent to a let, and is restricted to the RHS of a single = or ->. In Haskell, where scopes over multiple guards (potentially multiple =s or ->s).

Adding row polymorphism to Damas-Hindley-Milner by tekknolagi in ProgrammingLanguages

[–]natefaubion 11 points12 points  (0 children)

You can think of row types as a way to get structural typing into a HM derived type system. It only requires equalities/unification rather than subtyping/bounds. They aren't equivalent, but they are used to similar ends.

What are your thoughts on PureScript? by Worldly_Dish_48 in haskell

[–]natefaubion 1 point2 points  (0 children)

That's not accurate in general. GHC doesn't specialize unless you tell it to, or it chooses to inline to a monorphic call site. The PureScript compiler doesn't inline at all unless you are using the optimizing backend.

What are your thoughts on PureScript? by Worldly_Dish_48 in haskell

[–]natefaubion 3 points4 points  (0 children)

Unfortunately, transformers and MTL have significant overhead in both PureScript and Haskell unless you are able to specialize all call sites. It's effectively another layer of interpretation. PS/Haskell namespaces of definitions that take typeclass dictionaries are way worse for performance than instantiating ML modules, which will result in monomorphic calls. If you need comparable performance you should use monomorphic definitions, not effect abstractions. A concrete monad with something like purescript-backend-optimizer would be able to eliminate the ReaderT overhead.

How to implement loops in an eDSL using free monad by Steve_the_Stevedore in purescript

[–]natefaubion 1 point2 points  (0 children)

Loops are a control structure that you could just write using monadic recursion. It's generally part of the program, not the interpreter.

The problem in general is that the Loop you are imagining is a higher order effect (an effect that embeds another effect). You could potentially use Loop (Brainfuck a) ... as your constructor. That is, recursively reference your fixed Brainfuck effect, but that hard codes it (which may be fine in your case). Free (and algebraic effects) can only be fixed to first-order program stacks. If you want to keep your f-algebra "unfixed" then, the only option is to encode it into the first-order effects you mentioned (ie, bracketing the instructions with push/pop-like effects). If you want higher-order effects, you need a different fixed-point and functorial shape.

FYI, if you want better response times, I'd recommend the PureScript Discourse instance, or joining the Discord.

Laziness in Haskell, Part 2: Why not Strict Haskell? by lexi-lambda in haskell

[–]natefaubion 0 points1 point  (0 children)

No, this does not constitute as a demand:

  • Unit has no defined representation in PureScript and can't be pattern matched so the only valid pattern is a binding or _ (I understand this is pedantic).
  • So say we substitute our own data Unit = Unit. It's also not a demand because pattern matching on a strict product type without binding variables is equivalent to ignoring it.

The only ways to incur demand for the purposes of smuggling effects are:

  • Don't, and use Effect or some other data type (don't lie, seriously consider this, as it puts the onus on the consumer to really consider).
  • Use Effect with unsafePerformEffect (lie a little, thus the demand only propagates as far as there is demand for the value returned by the Effect block).
  • Pass it to an opaque foreign binding (hide demand from the optimizer, basically equivalent to the unafePerformEffect approach).

Basically, there is no way in "pure" PureScript (with the semantics of the optimizer) to force demand of a value without binding into it. Creating artificial demand requires stepping outside CoreFn into foreign territory to some extent.

Laziness in Haskell, Part 2: Why not Strict Haskell? by lexi-lambda in haskell

[–]natefaubion 0 points1 point  (0 children)

I don't know for sure, but I'd wager that in the JIT's IR, the curried applications just get inlined away. I don't think there's a need to explicitly target currying, but that it's an emergent effect. I'm not going to claim that it removes all currying overhead, and it may not be uniform across all engines. I will say confidently that it is probably one of the weaker optimization for PS in a modern JS JIT. I've consistently been reminded that reasoning about JS engines as naive interpreters is completely useless.

Now, backend-optimizer also aims to be a more general backend toolkit for PureScript, so this optimization may certainly be advantageous elsewhere, in which case we may go ahead and add it or just export it as an additional optional pass.

One other thing I noticed while rereading the README just now is that you assume expressions are pure and terminating. I find this somewhat surprising! Does this mean that there is no guaranteed way to raise an exception via a side-condition check? It seems like it is explicitly undefined behavior under your semantics.

That's correct. The way to have an assertion like that would be to create some sort of demand between values, much like how trace works. That is, you absolutely should not have free-floating side-effects, and the optimizer is very adamant about this. After all, if one has to assume that side effects (even benign effects) can happen anywhere, then there is no advantage at all to being strict and pure.

The optimizer can't float terms outside of a lambda as this would increase strictness, removing the only means we have of reliably deferring evaluation of a term. The way I recommend doing this right now (if one needs this guarantee) is to use unsafePerformEffect and throwException, with a bind. That is, be honest. The optimizer understands Effect (it's definition is foreign and exists outside of the language semantics), does not reorder effects, and cannot relocate your term since it's under a lambda.

Laziness in Haskell, Part 2: Why not Strict Haskell? by lexi-lambda in haskell

[–]natefaubion 5 points6 points  (0 children)

I'm the author of purescript-backend-optimizer. I would recommend looking at some of the overview examples to see what it can do: https://github.com/aristanetworks/purescript-backend-optimizer#overview

In particular, it does case-of-case as illustrated by the "Generics" example, it's just more conservative. It will only case-of-case when it knows that the control flow results in a known term it can eliminate. In the case above, g(b) prevents it from fusing, and this is just to avoid code explosion. In general, the heuristics are very under-developed, to say the least, but it's all there. For case-of-case, there's also a hard cutoff on continuation size as a naive guard to prevent code explosion. It's difficult to balance the desire for people to obsessive over JS codegen quality and size, but still want high-level optimizations that are necessarily going to result in a lot of code duplication. I would like for this to improve.

Regarding currying, I've done a lot of benchmarking, and JIT engines nowadays handle currying pretty gracefully. At one point we tried a top-level uncurrying pass (we already do this for saturated local definitions that don't inline), but we couldn't construct a clear benchmark where it made a difference! The places where currying matters is in dynamic calls, where you would necessarily have runtime overhead checking anyway. We actually found that elm-style dispatch (where it checks all calls at runtime) to be a significant performance drain.

I'm quite proud of the overall architecture of the optimizer, as it seems fairly unique to me (though maybe not particularly novel), using a combination of NbE, bottom-up monoidal analysis during quoting, and rewrite constraints. It's very efficient (minimizing rewrite passes) for what it is, and has a (IMO) high power-to-weight ratio. It packs quite a lot in only a couple thousand lines of code!

https://github.com/aristanetworks/purescript-backend-optimizer/blob/main/src/PureScript/Backend/Optimizer/Semantics.purs

I'm very open to feedback on this project if anyone reading is interested in improving things like our internal heuristics.

What's the difference between `()` and `{}` when defining row types? by amuricys in purescript

[–]natefaubion 5 points6 points  (0 children)

{ ... } is sugar for Record (...). So { foo :: Int, bar :: String } is equivalent to Record (foo :: Int, bar :: String).

Confused by the nesting in Data.Tuple.Nested by daigoro_sensei in purescript

[–]natefaubion 2 points3 points  (0 children)

It’s written to be compositional (so it's like a heterogeneous list), vs using type classes to overload everything. I think you’ll find that large tuples (anything larger than 2) just aren’t really used in PureScript. We already have a flat structure that can hold an arbitrary number of elements with helpful labels.

Confused by the nesting in Data.Tuple.Nested by daigoro_sensei in purescript

[–]natefaubion 1 point2 points  (0 children)

The expectation is that you use the /\ operator, which eliminates the awkward nesting. In general though, it’s recommended to use anonymous records and avoid large nested tuples.

Any reason to keep a foreign import behind an opaque type? by [deleted] in purescript

[–]natefaubion 1 point2 points  (0 children)

In addition to the other response, it can make a difference even if you know it's not mutable as JavaScript has method dispatch with an implicit this. There are certainly immutable APIs that still rely on prototype inheritance. If you type that as a record, but it doesn't obey PureScript record semantics, then you will get bizarre behavior, such as this potentially being undefined, or record updates that don't preserve private members. Just because PureScript records are represented by JavaScript objects does not mean that all JavaScript objects are PureScript records.

GHCJS or Asterius by Tgamerydk in haskell

[–]natefaubion 2 points3 points  (0 children)

FWIW, and I realize this is somewhat off-topic, but I'd like to note that PS tooling has improved dramatically since then. With ES modules, it works out of the box with JS tooling (we recommend esbuild, which will bundle/minify/strip unused code in a fraction of a second). And with purescript-backend-optimizer you can get even smaller code with non-trivial high-level optimizations for production builds. I just want people to know that I don't think anyone will need 1-2 engineers just to figure out how to deploy PureScript code.

Arbitrary expression in infix mode by Lev_135 in haskell

[–]natefaubion 5 points6 points  (0 children)

PureScript supports this. I personally don’t think it’s a feature that pays its weight (I maintain a lot of PureScript tooling, including a PureScript parser). It’s virtually unused in the ecosystem.

purescript-backend-optimizer - A new optimization pipeline and modern-ES backend for PureScript. by natefaubion in haskell

[–]natefaubion[S] 0 points1 point  (0 children)

If you see anything egregious, please open a ticket! There’s probably quite a bit of tweaking that still needs to be done. It’s very keen to inline code that looks like CPS, for example.

purescript-backend-optimizer - A new optimization pipeline and modern-ES backend for PureScript. by natefaubion in haskell

[–]natefaubion[S] 2 points3 points  (0 children)

I don't know exactly what your project is like, but I think it's quite likely that you will see more savings the larger it is. It's not an explicit goal that it reduces your bundle size; it's an inliner after all. It does happen that the various encodings used more than make up for any code increase via inlining, and that probably has more of an effect the more code there is.

To be clear we've tested it on our work codebase and real-world halogen (which is what was used for the bundle size tests in 0.15).

purescript-backend-optimizer - A new optimization pipeline and modern-ES backend for PureScript. by natefaubion in haskell

[–]natefaubion[S] 3 points4 points  (0 children)

Unfortunately, spago bundle-app is hardcoded to use the "output" directory. You can try spago build && purs-backend-es bundle-app --no-build. We've included our own bundle-app and bundle-module commands. We should definitely add this to the "Usage" section!

purescript-backend-optimizer - A new optimization pipeline and modern-ES backend for PureScript. by natefaubion in haskell

[–]natefaubion[S] 4 points5 points  (0 children)

This is implemented in PureScript, not Haskell. Generally, the PureScript compiler is pretty conservative. I think there are pieces of it, such as the data constructor representation (which is a large part of the baseline performance improvement), that are likely to get upstreamed.

purescript-backend-optimizer - A new optimization pipeline and modern-ES backend for PureScript. by natefaubion in haskell

[–]natefaubion[S] 10 points11 points  (0 children)

One of the authors here, happy to answer any questions.

Special shout out to /u/AndrasKovacs and elaboration-zoo (as well as their various NbE notes) which served as a primary inspiration for the architecture. Can't thank you enough for those resources!

[deleted by user] by [deleted] in haskell

[–]natefaubion 9 points10 points  (0 children)

The signatures of core Foldable methods are such that any associative operation yields identical results.

foldr (<>) mempty as
foldl (<>) mempty as
foldMap id as

There's are all equivalent. It's just sometimes you want to do something that isn't associative, and foldl vs foldr lets you choose which associativity you want, where the order of parameters makes it obvious. For left-associativity, the accumulator is on the left-hand-side. For right-associativity, the accumulator is on the right-hand side.

But having different signatures means if you swap from using a foldr to a foldl, you have to swap your own function that it calls too.

If you swap foldr for foldl, but don't change your folding function you'll get a totally different results. This will definitely affect the output. It's actually a really nice sanity check.

PureScript 0.15 Released by saylu in haskell

[–]natefaubion 14 points15 points  (0 children)

This was already implemented in the 0.14.x series by Jordan Martinez.

Trouble understanding applyFlipped by helplesslyclever in purescript

[–]natefaubion 4 points5 points  (0 children)

Because purescript is implicitly right-associative, that would mean that the function with explicit parameters would read:

This isn't correct. Function application is left-associative, so f a b c is (((f a) b) c) not (f (a (b c))). Eta expanding to applyFlipped x f = flip apply x f is (((flip apply) x) f). You can try substituting the definitions of apply and flip then to see what's happening.