Haskell-esque non-lazy web development lang that transpiles to javascript. Seems to be possible to develop for mobile as well.
May be installed via npm: npm -g install purescript spago
Partially gotten from here
- Strict evaluation
- inability to stuff declaration and assignment into the same line, i.e. this:
i :: Int = 1is legal in Haskell but not in PS. - explicit forall, i.e.
foo :: a -> atype annotation will result in error, it has to befoo :: forall a . a -> a. It supports∀though. StringandCharare UTF16 for some reason that goes back to JS.[a]syntax not supported, it has to beArray aor similar.- in
data Foo = Foo {a :: Int}theais not globally visible and instead you refer to it via a dot as a property. - function composition:
<<<instead of.. That's because dot is the property accessor. - deriving show, here are details why/how this code works:
import Data.Generic.Rep (class Generic) import Data.Show.Generic (genericShow) data Foo = Foo{a :: Int} derive instance genericMyADT :: Generic Foo _ instance showMyADT :: Show Foo where show = genericShow
returnreplaced withpure- "Records" are not
dataand work somewhat differently. Records enclosed into braces and then their fields are referred to via a dot. But there's more to it. Given this example:First two fields are referred to byfoo = {a : 1, "b" : 2, "A" : 3, "A B" : 4}foo.aandfoo.band are basically the same. But the other two can't be referred directly as such and instead this syntax is usedfoo."A",foo."A B".
- In TS
constandreadonlyworks arbitrary. It won't disallow you to assign intoconstobject or an array.- separately worth noting that even that is implemented poorly. If you want to declare a
constparameter in a function, you can't just declare itconst, but you instead have to do that via genericfunction f<const T extends string>(arg: T).
- separately worth noting that even that is implemented poorly. If you want to declare a
- In TS equality works arbitrary. Comparing objects of different classes or interfaces or
types ignores their types, and just looks up the fields. If they match, you'll get no type mismatch. Worth pointing out it also stands for function parameters, not just comparisons. This has huge implications, because not only simply class objects are unreliable, but also (or rather "especially") unions of different types. And you also can't declare a simple wrapper type such asDegreevsRadian, well not without some tricks withunique symbolfield. - TS has exceptions. This is a large separate topic, but exceptions are generally frowned upon. Rust doesn't even include them.
- TS has no syntax for do-notation
- TS has no currying. "Who cares?" you might say, but bear with me: I looked at some production TS code using React, and it seems TS React programmers frequently create chains of
lambdacalls just because the lack of currying. So the feature is actually needed. - ADTs *(like
data)*are complicated to implement. You basically have to write a union, where each field is an object type with an explicit tag. - There are some problems with Higher Kinded Types. An example of HKT is
Array<T>, where typeArraytakes a type-parameterT. Now,Arrayis a predeclared HKT, but creating a new one such asFunctor<T>results in some problems, which I can't give details about because I didn't dig into it (just read that in a post with possible workarounds).
- tools:
- current list, may be useful because some tools are deprecated by others.
pursthe compilerspagoa build tool for PS, there are: stable non-developed (Haskell-based) and unstable actively developed (PS-based) versions.pulpan older build tool for PS, was used together withbowerbeforespago.
- "array comprehension":
import Data.Arrayand then use e.g.1 .. 5. - async API is provided by
Effect.Affand starts withlaunchAffwhich convertsAff a → Effect a. logShow(fromEffect.Class.Console): to print fromAffto console. At least with HTTPurple it shows up in terminal as expected.
A backend lib for processing http methods.
- URL path/queries parsing is called "routing"
- routes are declared as a record passed to
mkRoutefunction. The record content is basically constructing the URL. Example from the docs:
route :: RouteDuplex' Route route = mkRoute { "Home": noArgs -- the root route / , "Products": "categories" / string segment / "products" / string segment , "Search": "search" ? { q: string, sorting: optional <<< string } } - routes are declared as a record passed to
(note: don't use it, use React instead, see the comparison further below)
A library for UI in html + js.
-
HTML tags are created by calling a function that creates a tag and passing it two arrays: 1. properties for the tag 2. children. Example, if we want this HTML:
<div id="root"> <input placeholder="Name" /> <button class="btn-primary" type="submit"> Submit </button> </div>
we code it like this:
import Halogen.HTML as HH import Halogen.HTML.Properties as HP html = HH.div [ HP.id "root" ] [ HH.input [ HP.placeholder "Name" ] , HH.button [ HP.classes [ HH.ClassName "btn-primary" ] , HP.type_ HP.ButtonSubmit ] [ HH.text "Submit" ] ]
-
underscores: when HTML and PS keywords clash, Halogen adds an underscore in the name, e.g.
type_. But then Halogen has also shortcut-functions ending with underscore for when you pass no properties, so instead ofdiv [] …you can writediv_ … -
caching: inside component
handleAction, ifmodify_ \state -> …is called, thestateis the cache. It will later be the input torender.
First, bad news: if you had read Halogen praises about how it's good in type-safety and well designed, well, this is where that ends. The messaging part is a bunch of useless abstractions where you can easily forget something and stuff silently breaks. For example, you can forget to insert the useless receive = Just <<< Foo, and everything will compile just fine but child won't be receiving inputs the parent sends it. Usually, with such huge abstractions you'd expect support for monkey-typing because there's too much to bear in mind, but for some funny reason Halogen is exactly the framework where it doesn't work. You have to study all those useless abstractions and make sure you got them right, or expect hours in debugging.
Given two components (things created by mkComponent), they can exchange kind of like signals with optional data. Here, the parent is the component that inserts the other one via slot function.
slot inserts a component similarly to how a HH.someTag would insert a tag. Args: given a call slot id subId component input mapChildOutputToParent:
-
id: a unique slot name defined via type-level magic like_button = Proxy :: Proxy "button". The text should match the name insidetype Slots = ( button :: ButtonSlot Unit ), where thisSlotstype is being used in the parent'srenderandhandleActionfunctions.Purpose: it's given to
H.tellfunction to send some signal/data to the slot. -
subid: in case you'd like to render the component multiple times, you can distinguish them bysubid. Passunitif not interested. -
component: the child created bymkComponent -
input: data to be passed to child'sinitialState -
mapChildOutputToParent: a function that takes child output and produces a parent "action", the one thathandleActiontakes as parameter. Typically it's just a parent-action data constructor that wraps the output.
slot_ is similar to slot but with the output omitted, for cases where the child doesn't produce anything a parent would be interested in.
Child must provide to H.defaultEval a field receive = Just <<< Foo where Foo is a data constructor ChildInput -> ChildState, and then the data constructor is handled as the parameter to handleAction. The receive = … is the key — you'd think handleAction should be enough, but apparently Halogen authors decided it should be confusing and error-prone, so they introduced this useless proxy-method that serves no purpose and you can easily forget to add it. Though, it can be omitted when input is only provided via tell (so e.g. you initialize the slot with unit).
With that out of the way, there are two ways to give an input to a child:
-
Implicit (bad): inside parent you call
H.modify_to modify some state that insiderendergets passed to the child. This is error-prone, because if compiler determines the state wasn't modified it won't trigger the child. This is a problem when you have no input for the child but just want to signal it. But it's also a problem for when there is an input but child does something besides. Imagine invoking a modal window for certain data. If a user dismissed the window, Halogen won't ever invoke it again till the data changes. -
Explicit (good): parent calls
tell id subId QueryConstructorwhere first two parameters are mentioned before andQueryConstructoris a constructor that may or may not pass some value, but the last mandatory argument you leave empty and then it's returned aspure (Just next). Idk what it's for.For this to work you need to declare a separate function
handleQuery, similar tohandleAction, but taking theQueryConstructortype instead. ApparentlyhandleActionwasn't enough for the authors, so now you need to bearhandleQueryin mind as well, because if you forget to declare it thinking abouthandleAction, stuff will just silently break. Example:
data ButtonQuery a = SetEnabled a
mycomponent = …
handleQuery :: forall a . ButtonQuery a -> H.HalogenM ButtonState ButtonAction () Unit m (Maybe a)
handleQuery (SetEnabled next) = do
…do something…
pure (Just next)Libs are ultimately an FFI-shim to a JS library, implying that if you ever get stuck beyond the basics, you can often search for solutions in JS-field and interpolate to PS. There're also examples here, see dirs with "react" infix.
There're two implementations: react-basic-classic and react-basic-hooks. The "classic" is a class-based implementation (pun is noted) that predates "hooks". Nowadays "hooks" are preferred.
-
atomic nodes (a button, label, etc) are represented by
JSXtype. -
"component" is a single React-managed DOM-tree (made of
JSXes and handlers)Componentis the type, which is an alias toEffect (props -> JSX). Thepropsis an arg to be passed when instancing the component withrenderRoot.- Running
Componentis done by unwrapping fromEffectand passing over torenderRoot.
-
"root" is a location for the first component to attach with
renderRoot. Created bycreateRoot. There may be many roots.Bear in mind, just nesting components doesn't require creating new roots.
-
Nesting components example (a label inside a div):
labelComponent :: Component Unit labelComponent = component "Label" \_ -> do pure $ R.label_ [ R.text "Hello, world!" ] divComponent :: Component Unit divComponent = do c :: (Unit -> JSX) <- labelComponent component "Div" \_ -> do pure $ R.div_ [c unit]
- Much simpler. You can read around for comparison, but in short: Halogen requires you to build inconvenient and error-prone abstractions; code reuse with Halogen is complicated. Making a generic component that accepts parameters and returns something back is so inconvenient that unless you come up with some crafty wrappers, you'll find easier to write a component each time anew than factor out existing ones to something generic.
- React has special
CSStype, whereas Halogen has just a string instead. - Halogen doesn't allow to execute
Effectbefore rendering the initial state. So you have to jump through the hoops by assigning useless "initial state" which gets immediately replaced by the actual state inhandleAction&co. In React you just execute what you need inComponentand then pass it over to the lambda that will be creating the component. - React elements (
JSXes) areMonoid, Halogen's aren't. This simplifies conditionally rendering elements: instead of doing a[multiple, children] <> if a then [anotherElem] else []you just write[multiple, children, guard anotherElem], whereguardis the Monoid's. Much shorter, huh? - Halogen's "Ref"s require you to name them, whereas React's don't. So React refs can't collide, whereas in a big Halogen project you can come up with name that was already used.
- Alternating branches via
<|>should only be done at the top of do-block, otherwise failure wouldn't propagate properly. This works the same in original Parsec. See this for details.
- QuickCheck: for property-based testing, randomly generates tests that check given function properties.
- Spec: a usual testing framework. Provides different runners, one is node-based
node-specpackage.
Basic example:
module Test.Main where
import Prelude
import Effect (Effect)
import Test.Spec (Spec, it)
import Test.Spec.Assertions (shouldEqual)
import Test.Spec.Reporter (consoleReporter)
import Test.Spec.Runner.Node (runSpecAndExitProcess)
main :: Effect Unit
main = runSpecAndExitProcess [consoleReporter] spec
spec :: Spec Unit
spec = do
it "adds 1 and 1" $ (1 + 1) `shouldEqual` 2
it "adds 2 and 2" $ (2 + 2) `shouldEqual` 4The it inside spec function are the separate tests.
There's also some spec-discovery for automatically discovering tests, but for me it wasn't finding some output/cache-db.json/index.js after following the docs and I didn't dig into that.
It may be desirable (e.g. for FFI purposes) to define some type Props = {x :: Int, y :: Int} but then to be able to pass just {x: 7} to some function, i.e. so y is not defined in the parameter.
It's useful to look at naive attempt first. Param here doesn't provide "predefined" fields, instead being "any possible record":
foo :: ∀ a. Record a -> Effect Unit
foo = const unit
main = foo {x: 7}We can improve it with some type-magic. PureScript has class Union lhs rhs theUnion which allows to declare a union of lhs and rhs. It works on Rows, but Record accepts one type-parameter that is a Row.
So we declare Props as a Row (parentheses instead of curly braces), and in function declaration say that Props is a union of any two possible Rows; and then as the parameter we use Record lhs (may also be Record rhs, doesn't matter), which basically says that the parameter is a record with any field enlisted in Props, which is exactly what we want.
type Props = (x :: Int, y :: Int)
foo :: ∀ lhs rhs. Union lhs rhs Props => Record lhs -> Effect Unit
foo _ = pure unit
main = foo {x: 7}It's interesting to note here that Props is used implicitly. The function parameter is just "some" type with type-constraint related to Props.
spago bundle-app produces a JS, which may be run with node or a browser (by augmenting it with index.html), and you can add console.log()s in it. In the JS stuff is getting renamed/mangled, but there is some structure to you can follow:
mkFnXfunctions are translated to afunction (…) {}. E.g.mkFn2 \state2@(ParseState _ _ consumed) err -> …is translated tofunction(v4, err) { … }.runFnXfunction are translated toreturn someVal(…). E.g.runFn5 k1 (ParseState input pos false) more lift foo doneis translated toreturn v(new ParseState(v2.value0, v2.value1, false), more, lift1, foo, done);