Strongly-typed front-end: experiment 2, simple application, in PureScript
Contents
- Introduction
- Experiment 1, darken_color
- Experiment 2, simple application
In PureScript world there are quite a few libraries for React. And all of them have terrible (or rather non-existent) documentation, so I had to use as much intuition as outdated and barely working code samples. In this case I have picked purescript-react-dom.
Initial application structure:
module Main where
import Prelude
import Control.Monad.Eff
import Data.Maybe
import Data.Maybe.Unsafe (fromJust)
import Data.Nullable (toMaybe)
import Effect (Effect)
import Effect.Console (log)
import DOM (DOM())
import DOM.HTML (window)
import DOM.HTML.Document (body)
import DOM.HTML.Types (htmlElementToElement)
import DOM.HTML.Window (document)
import DOM.Node.Types (Element())
import React
import React.DOM as DOM
import React.DOM.Props as Props
type Shape = Circle | Square
calculateArea :: Maybe Shape -> Float -> Float
calculateArea Nothing _ = 0
calculateArea (Just Circle) value = pi * value * value
calculateArea (Just Square) value = value * value
getShape :: String -> Maybe Shape
getShape "circle" = Just Circle
getShape "square" = Just Square
getShape _ = Nothing
onShapeChanged ctx evt = do
writeState ctx { shape: getShape ((unsafeCoerce evt).target.value) }
onCalculateAreaClicked ctx evt = do
{ shape, value } <- readState ctx
writeState ctx { area: calculateArea shape value }
areaCalculator = createClass $ spec { shape: Nothing, value: 0, area: 0 } \ctx -> do
{ shape, value, area } <- readState ctx
return $ DOM.div [] [
DOM.div [] [
DOM.select [ Props.onChange (onShapeChanged ctx) ] [
DOM.option [ Props.value "" ] [ DOM.text "Select shape" ],
DOM.option [ Props.value "circle" ] [ DOM.text "Circle" ],
DOM.option [ Props.value "square" ] [ DOM.text "Square" ]
],
DOM.input [ Props.value (show value) ] [],
DOM.button [ Props.onClick (onCalculateAreaClicked ctx) ] [ DOM.text "Calculate area" ]
],
DOM.div [] [
DOM.text ("Area: " ++ (show area))
]
]
main = container >>= render ui
where
ui :: ReactElement
ui = createFactory areaCalculator {}
container :: forall eff. Eff (dom :: DOM | eff) Element
container = do
win <- window
doc <- document win
elt <- fromJust <$> toMaybe <$> body doc
return $ htmlElementToElement elt
Immediately the flaws of the infrastructure come out:
$ spago build 1 ↵
Error 1 of 8:
in module Main
at src/Main.purs:5:1 - 5:25 (line 5, column 1 - line 5, column 25)
Module Control.Monad.Eff was not found.
Make sure the source file exists, and that it has been provided as an input to the compiler.
See https://github.com/purescript/documentation/blob/master/errors/ModuleNotFound.md for more information,
or to contribute content related to this error.
Error 2 of 8:
in module Main
at src/Main.purs:8:1 - 8:36 (line 8, column 1 - line 8, column 36)
Module Data.Maybe.Unsafe was not found.
Make sure the source file exists, and that it has been provided as an input to the compiler.
See https://github.com/purescript/documentation/blob/master/errors/ModuleNotFound.md for more information,
or to contribute content related to this error.
Error 3 of 8:
in module Main
at src/Main.purs:14:1 - 14:19 (line 14, column 1 - line 14, column 19)
Module DOM was not found.
Make sure the source file exists, and that it has been provided as an input to the compiler.
See https://github.com/purescript/documentation/blob/master/errors/ModuleNotFound.md for more information,
or to contribute content related to this error.
Error 4 of 8:
in module Main
at src/Main.purs:15:1 - 15:25 (line 15, column 1 - line 15, column 25)
Module DOM.HTML was not found.
Make sure the source file exists, and that it has been provided as an input to the compiler.
See https://github.com/purescript/documentation/blob/master/errors/ModuleNotFound.md for more information,
or to contribute content related to this error.
Error 5 of 8:
in module Main
at src/Main.purs:16:1 - 16:32 (line 16, column 1 - line 16, column 32)
Module DOM.HTML.Document was not found.
Make sure the source file exists, and that it has been provided as an input to the compiler.
See https://github.com/purescript/documentation/blob/master/errors/ModuleNotFound.md for more information,
or to contribute content related to this error.
Error 6 of 8:
in module Main
at src/Main.purs:17:1 - 17:45 (line 17, column 1 - line 17, column 45)
Module DOM.HTML.Types was not found.
Make sure the source file exists, and that it has been provided as an input to the compiler.
See https://github.com/purescript/documentation/blob/master/errors/ModuleNotFound.md for more information,
or to contribute content related to this error.
Error 7 of 8:
in module Main
at src/Main.purs:18:1 - 18:34 (line 18, column 1 - line 18, column 34)
Module DOM.HTML.Window was not found.
Make sure the source file exists, and that it has been provided as an input to the compiler.
See https://github.com/purescript/documentation/blob/master/errors/ModuleNotFound.md for more information,
or to contribute content related to this error.
Error 8 of 8:
in module Main
at src/Main.purs:20:1 - 20:34 (line 20, column 1 - line 20, column 34)
Module DOM.Node.Types was not found.
Make sure the source file exists, and that it has been provided as an input to the compiler.
See https://github.com/purescript/documentation/blob/master/errors/ModuleNotFound.md for more information,
or to contribute content related to this error.
[error] Failed to build.
Well, the error messages kind of point you to the source of error - the modules have not been provided to the compiler.
Had to use documentation for each of the packages to match the types and fix the imports (since the example I have relied upon is way out of date):
$ spago install purescript-web-dom purescript-web-html react-dom
And adjust the code itself:
module Main where
import Prelude
import Effect (Effect)
import Data.Maybe
import Web.HTML (window)
import Web.HTML.HTMLDocument (body)
import Web.HTML.HTMLElement (toElement)
import Web.HTML.Window (document)
import Web.DOM.Element (Element())
import React as React
import ReactDOM as ReactDOM
import React.DOM as DOM
import React.DOM.Props as Props
type Shape = Circle | Square
calculateArea :: Maybe Shape -> Float -> Float
calculateArea Nothing _ = 0
calculateArea (Just Circle) value = pi * value * value
calculateArea (Just Square) value = value * value
getShape :: String -> Maybe Shape
getShape "circle" = Just Circle
getShape "square" = Just Square
getShape _ = Nothing
onShapeChanged ctx evt = do
writeState ctx { shape: getShape ((unsafeCoerce evt).target.value) }
onCalculateAreaClicked ctx evt = do
{ shape, value } <- React.readState ctx
writeState ctx { area: calculateArea shape value }
areaCalculator :: React.ReactClass { }
areaCalculator = React.component "AreaCalculator" component
where
component ctx = pure { state: { shape: Nothing, value: 0, area: 0 }, render: renderFn ctx }
where
renderFn ctx =
{ shape, value, area } <- React.readState ctx
return $ DOM.div [] [
DOM.div [] [
DOM.select [ Props.onChange (onShapeChanged ctx) ] [
DOM.option [ Props.value "" ] [ DOM.text "Select shape" ],
DOM.option [ Props.value "circle" ] [ DOM.text "Circle" ],
DOM.option [ Props.value "square" ] [ DOM.text "Square" ]
],
DOM.input [ Props.value (show value) ] [],
DOM.button [ Props.onClick (onCalculateAreaClicked ctx) ] [ DOM.text "Calculate area" ]
],
DOM.div [] [
DOM.text ("Area: " ++ (show area))
]
]
main = container >>= ReactDOM.render componentInstance
where
componentInstance = React.createLeafElement areaCalculator {}
container :: forall eff. Effect (dom :: DOM | eff) Element
container = do
win <- window
doc <- document win
elt <- body doc
return $ toElement elt
To get yet another error:
Error found:
at src/Main.purs:22:21 - 22:22 (line 22, column 21 - line 22, column 22)
Unable to parse module:
Unexpected token '|'
That’s my bad, it is more Haskell than Elm or F#:
- type Shape = Circle | Square
+ data Shape = Circle | Square
And yet another one:
Error found:
at src/Main.purs:45:3 - 45:8 (line 45, column 3 - line 45, column 8)
Unable to parse module:
Unexpected token 'where'
Because I did not indent my code enough.
And yet another one:
Error found:
at src/Main.purs:47:32 - 47:34 (line 47, column 32 - line 47, column 34)
Unable to parse module:
Unexpected "<-" in expression, perhaps due to a missing 'do' or 'ado' keyword
Because React.readState
does not return effect, but rather the value itself, so no need to unpack it:
- { shape, value, area } <- React.readState ctx
+ let { shape, value, area } = React.readState ctx
And yet another one:
Error found:
at src/Main.purs:48:9 - 48:15 (line 48, column 9 - line 48, column 15)
Unable to parse module:
Unexpected token 'return'
Because I have forgot the do
keyword:
- renderFn ctx =
+ renderFn ctx = do
Few small errors resolved:
Compiling Main
Error 1 of 2:
in module Main
at src/Main.purs:26:33 - 26:38 (line 26, column 33 - line 26, column 38)
Unknown type Float
# replace Float with Number, as Float is not really a type in PureScript
Error 2 of 2:
in module Main
at src/Main.purs:61:32 - 61:34 (line 61, column 32 - line 61, column 34)
Unknown operator (++)
# replace ++ with + as ++ is not really an operator in PureScript
To end up with type errors hell:
Error found:
in module Main
at src/Main.purs:60:13 - 60:54 (line 60, column 13 - line 60, column 54)
Could not match type
ReactElement
with type
Array t0 -> t1
while applying a function input [ value ((...) value)
]
of type ReactElement
to argument []
while checking that expression (input [ value (...)
]
)
[]
has type ReactElement
in value declaration areaCalculator
where t0 is an unknown type
t1 is an unknown type
What this tells you is that input function does not take a second argument (which would normally be similar to React.DOM.div [ attributes ] [ children ]
).
- DOM.input [ Props.value (show value) ] []
+ DOM.input [ Props.value (show value) ]
Earlier I have mentioned type hell. Well, that was just the small example. Here’s the next error in the code:
Error found:
in module Main
at src/Main.purs:46:18 - 46:50 (line 46, column 18 - line 46, column 50)
Could not match type
Int
with type
Number
while trying to match type
( area :: Int
, shape :: Maybe t3
, value :: Int
...
)
with type
( area :: Number
, shape :: Maybe Shape
, value :: Number
...
| t1
)
while solving type class constraint
Prim.Row.Nub t0
( componentDidCatch :: Error
-> { componentStack :: String
}
-> Effect Unit
, componentDidMount :: Effect Unit
, componentDidUpdate :: Record ()
-> { area :: Number
, shape :: ...
, value :: Number
| t1
}
-> t2 -> ...
, componentWillUnmount :: Effect Unit
, getSnapshotBeforeUpdate :: Record ()
-> { area :: Number
, shape :: ...
, value :: Number
| t1
}
-> Effect t2
, render :: Effect ReactElement
, shouldComponentUpdate :: Record ()
-> { area :: Number
, shape :: ...
, value :: Number
| t1
}
-> Effect Boolean
, state :: { area :: Number
, shape :: Maybe Shape
, value :: Number
| t1
}
, unsafeComponentWillMount :: Effect Unit
, unsafeComponentWillReceiveProps :: Record () -> Effect Unit
, unsafeComponentWillUpdate :: Record ()
-> { area :: Number
, shape :: ...
, value :: Number
| t1
}
-> Effect Unit
)
while inferring the type of component "AreaCalculator"
in value declaration areaCalculator
where t1 is an unknown type
t3 is an unknown type
t0 is an unknown type
t2 is an unknown type
See https://github.com/purescript/documentation/blob/master/errors/TypesDoNotUnify.md for more information,
or to contribute content related to this error.
Basically compiler is trying to say the initial state provided has a type ( area :: Int, value :: Int, shape :: Maybe t3 )
but what is expected (down in the code) is ( area :: Number, value :: Number, shape :: Maybe Shape )
. Quite an explanation.
+ initialState :: AreaCalculatorState
+ initialState = { shape: Nothing, value: 0.0, area: 0.0 }
- componentImpl ctx = pure { state: { shape: Nothing, value: 0.0, area: 0.0 }, render: renderFn ctx }
+ componentImpl ctx = pure { state: initialState, render: renderFn ctx }
And back to a new error:
Error found:
in module Main
at src/Main.purs:70:23 - 70:45 (line 70, column 23 - line 70, column 45)
No type class instance was found for
Data.Semiring.Semiring String
while applying a function add
of type Semiring t0 => t0 -> t0 -> t0
to argument "Area: "
while inferring the type of add "Area: "
in value declaration areaCalculator
where t0 is an unknown type
See https://github.com/purescript/documentation/blob/master/errors/NoInstanceFound.md for more information,
or to contribute content related to this error.
Which means the +
operator (function) does not apply to strings. The <>
is the string concatenation operator in this case. Although very hectic, it is described in the docs.
And yet another error:
Error found:
in module Main
at src/Main.purs:78:21 - 78:29 (line 78, column 21 - line 78, column 29)
Could not match type
Effect
with type
Maybe
while trying to match type Effect (Maybe HTMLElement)
with type Maybe t0
while checking that expression body doc
has type Maybe t0
in value declaration main
where t0 is an unknown type
See https://github.com/purescript/documentation/blob/master/errors/TypesDoNotUnify.md for more information,
or to contribute content related to this error.
Which is all about matching the return types and handling them properly:
- elt <- toElement elt
+ let elt = fromJust eltMaybe
+ let container = toElement elt
And finally, the version that compiles:
module Main where
import Prelude
import Effect (Effect)
import Data.Maybe
import Math (pi)
import Web.HTML (window)
import Web.HTML.HTMLDocument (body)
import Web.HTML.HTMLElement (toElement)
import Web.HTML.Window (document)
import Web.DOM.Element (Element())
import Unsafe.Coerce (unsafeCoerce)
import React as React
import ReactDOM as ReactDOM
import React.DOM as DOM
import React.DOM.Props as Props
data Shape = Circle | Square
calculateArea :: Maybe Shape -> Number -> Number
calculateArea Nothing _ = 0.0
calculateArea (Just Circle) value = pi * value * value
calculateArea (Just Square) value = value * value
getShape :: String -> Maybe Shape
getShape "circle" = Just Circle
getShape "square" = Just Square
getShape _ = Nothing
onShapeChanged ctx evt = do
React.setState ctx { shape: getShape ((unsafeCoerce evt).target.value) }
onCalculateAreaClicked ctx evt = do
{ shape, value } <- React.getState ctx
React.setState ctx { area: calculateArea shape value }
type AreaCalculatorState = { shape :: Maybe Shape, value :: Number, area :: Number }
initialState :: AreaCalculatorState
initialState = { shape: Nothing, value: 0.0, area: 0.0 }
areaCalculator :: React.ReactClass { }
areaCalculator = React.component "AreaCalculator" componentImpl
where
componentImpl ctx = pure { state: initialState, render: renderFn ctx }
where
renderFn ctx' = do
{ shape, value, area } <- React.getState ctx'
pure $ DOM.div [] [
DOM.div [] [
DOM.select [ Props.onChange (onShapeChanged ctx') ] [
DOM.option [ Props.value "" ] [ DOM.text "Select shape" ],
DOM.option [ Props.value "circle" ] [ DOM.text "Circle" ],
DOM.option [ Props.value "square" ] [ DOM.text "Square" ]
],
DOM.input [ Props.value (show value) ],
DOM.button [ Props.onClick (onCalculateAreaClicked ctx') ] [ DOM.text "Calculate area" ]
],
DOM.div [] [
DOM.text ("Area: " <> (show area))
]
]
main = do
let componentInstance = React.createLeafElement areaCalculator {}
win <- window
doc <- document win
eltMaybe <- body doc
let elt = fromJust eltMaybe
let container = toElement elt
ReactDOM.render componentInstance container
Of course, there are few warnings and few sloppy solutions worth fixing:
Warning 1 of 7:
in module Main
at src/Main.purs:7:1 - 7:18 (line 7, column 1 - line 7, column 18)
Module Data.Maybe has unspecified imports, consider using the explicit form:
import Data.Maybe (Maybe(..), fromJust)
See https://github.com/purescript/documentation/blob/master/errors/ImplicitImport.md for more information,
or to contribute content related to this warning.
Warning 2 of 7:
in module Main
at src/Main.purs:5:1 - 5:23 (line 5, column 1 - line 5, column 23)
The import of Effect is redundant
See https://github.com/purescript/documentation/blob/master/errors/UnusedImport.md for more information,
or to contribute content related to this warning.
Warning 3 of 7:
in module Main
at src/Main.purs:3:1 - 3:15 (line 3, column 1 - line 3, column 15)
Module Prelude has unspecified imports, consider using the explicit form:
import Prelude (bind, pure, show, ($), (*), (<>))
See https://github.com/purescript/documentation/blob/master/errors/ImplicitImport.md for more information,
or to contribute content related to this warning.
Warning 4 of 7:
in module Main
at src/Main.purs:16:1 - 16:35 (line 16, column 1 - line 16, column 35)
The import of Web.DOM.Element is redundant
See https://github.com/purescript/documentation/blob/master/errors/UnusedImport.md for more information,
or to contribute content related to this warning.
Warning 5 of 7:
in module Main
at src/Main.purs:38:1 - 39:75 (line 38, column 1 - line 39, column 75)
No type declaration was provided for the top-level declaration of onShapeChanged.
It is good practice to provide type declarations as a form of documentation.
The inferred type of onShapeChanged was:
forall t10 t14 t8.
ReactThis t10
{ shape :: ...
| t8
}
-> t14 -> Effect Unit
in value declaration onShapeChanged
See https://github.com/purescript/documentation/blob/master/errors/MissingTypeDeclaration.md for more information,
or to contribute content related to this warning.
Warning 6 of 7:
in module Main
at src/Main.purs:41:1 - 43:57 (line 41, column 1 - line 43, column 57)
No type declaration was provided for the top-level declaration of onCalculateAreaClicked.
It is good practice to provide type declarations as a form of documentation.
The inferred type of onCalculateAreaClicked was:
forall t24 t37 t39.
ReactThis t37
{ area :: Number
, shape :: ...
, value :: Number
| t39
}
-> t24 -> Effect Unit
in value declaration onCalculateAreaClicked
See https://github.com/purescript/documentation/blob/master/errors/MissingTypeDeclaration.md for more information,
or to contribute content related to this warning.
Warning 7 of 7:
in module Main
at src/Main.purs:74:1 - 81:46 (line 74, column 1 - line 81, column 46)
No type declaration was provided for the top-level declaration of main.
It is good practice to provide type declarations as a form of documentation.
The inferred type of main was:
Partial => Effect (Maybe ReactComponent)
in value declaration main
See https://github.com/purescript/documentation/blob/master/errors/MissingTypeDeclaration.md for more information,
or to contribute content related to this warning.
Essentially,
Module Data.Maybe has unspecified imports, consider using the explicit form:
import Data.Maybe (Maybe(..), fromJust)
is fixed with the suggested code:
import Data.Maybe (Maybe(..), fromJust)
Same with the unnecessary imports.
This one looks neat:
Warning 5 of 7:
in module Main
at src/Main.purs:38:1 - 39:75 (line 38, column 1 - line 39, column 75)
No type declaration was provided for the top-level declaration of onShapeChanged.
It is good practice to provide type declarations as a form of documentation.
The inferred type of onShapeChanged was:
forall t10 t14 t8.
ReactThis t10
{ shape :: ...
| t8
}
-> t14 -> Effect Unit
in value declaration onShapeChanged
See https://github.com/purescript/documentation/blob/master/errors/MissingTypeDeclaration.md for more information,
or to contribute content related to this warning.
Compiler asks you to explicitly type the function declaration. But I don’t care about that for now.
In order to run the app, few actions are still needed:
$ yarn add -D spago purescript parcel
Then, one will need an entry point:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="index.js"></script>
</body>
</html>
with a script:
const Main = require('./output/Main');
Main.main();
To build that whole thing now:
$ yarn spago build && yarn parcel index.html
And finally, if you run this, nothing will work.
All because the return type of the main
function is not really Effect Unit
, but rather a function. To fix this:
mountMain :: HTMLElement -> Effect Unit
mountMain elt = do
let container = toElement elt
let componentInstance = React.createLeafElement areaCalculator {}
let componentMaybe = ReactDOM.render componentInstance container
void componentMaybe
main :: Effect Unit
main = do
win <- window
doc <- document win
eltMaybe <- body doc
maybe (pure unit) mount eltMaybe
And the thing still won’t completely work, since we do not modify the state on input value change:
import Data.Float.Parse (parseFloat)
onValueChanged ctx evt = do
let newValue = fromMaybe 0.0 (parseFloat ((unsafeCoerce evt).target.value))
React.setState ctx { value: newValue }
Final solution is available on sandbox.