Helpful Tips
Special Haskell Syntax used by IHP
IHP uses many “exotic” haskell features. Here’s a short explanation of the most common. In case you think something is missing, let us know on Slack.
The hash symbols #
IHP uses hash symbols #
all over the place, like in this code:
set #companyId company.id
The hashes are provided by the OverloadedLabels
language extension.
In IHP you can think of these hash-strings as immutable type-level strings. One of the most common uses for labels is to represent field names. In the above example #companyId
refers to the companyId
field on the Company
data structure.
Technical details:
Writing #companyId
is equivalent to writing fromLabel @"companyId"
. The fromLabel
function is provided by a type class and can be implemented by e.g. libraries and frameworks. In IHP this instance is usually this:
instance IsLabel name (Proxy name') where
fromLabel = Proxy @name'
So #companyId
can be written as fromLabel @"companyId"
which IHP turns into Proxy @"companyId"
. The Proxy
value is now a normal haskell value and is passed to functions such as get
or set
.
The dot notation .
IHP uses dot notation like someRecord.someField
everywhere. You might not think of this as anything special, but this syntax was just recently added to Haskell.
The dot notation is provided by the OverloadedRecordDot
language extension.
In IHP v0.19 and older IHP versions, the dot notation was not supported yet. So you might see get #someField someRecord
instead in older projects. The get #someField someRecord
notation is equivalent to someRecord.someField
and is still supported:
-- IHP v0.19 and before:
get #title project
-- IHP v0.20 and up:
project.title
The at symbol @
Another symbol used often in IHP is the @
symbol, like this:
action UsersAction = do
users <- query @User |> fetch
render IndexView { .. }
This is called a Type application.
If you have open questions about lists like fill @["title", "body"]
, after reading the linked blog post:
These are type-level lists. You can write @"type level strings"
and type level integers like @1337
, so you can also write type level lists like @["title", "body"]
.
Be aware: When you write a type level list like @["title"]
, so with only a single element, you need to prepend a '
like this @'["title"]
. Otherwise you’ll get an error. We opened a ticket on the haskell compiler for this already.
The { .. }
Inside actions the views are usually rendered like this:
action UsersAction = do
users <- query @User |> fetch
render IndexView { .. }
We assume that the view is defined like this:
data IndexView = IndexView { users :: [User] }
Then the expression IndexView { .. }
is a shortcut for IndexView { users = users }
.
When the haskell compiler expands this, it will internally be like this:
action UsersAction = do
users <- query @User |> fetch
render IndexView { users = users }
You can learn more here about these so-called Record wildcards
.
The bang operator !
Inside your Web/Types.hs
you see lots of !
s, like this:
ShowPostAction { postId :: !(Id Post) }
The !
marks the postId
field as strict. Strict means that the value of postId
is not stored as a lazy value. So instead of only computing the postId
field when needed, it will already be fully computed when the ShowPostAction
value is constructed. Basically we tell haskell that we’re sure that this field is always needed. This makes the action data structures more memory efficient.
You can learn more about strictness and lazyness in haskell in this blog post.
The \case
Inside controllers you typically find code like this:
user
|> fill @["firstname", "lastname"]
|> validateField #firstname nonEmpty
|> ifValid \case
Left user -> render EditView { .. }
Right user -> do
user <- user |> updateRecord
redirectTo EditUserAction { .. }
The \case
is called a Lambda case
in haskell. It’s a shortcut for writing this:
user
|> fill @["firstname", "lastname"]
|> validateField #firstname nonEmpty
|> ifValid (\user -> case user of
Left user -> render EditView { .. }
Right user -> do
user <- user |> updateRecord
redirectTo EditUserAction { .. }
)
In general the \case ...
can be expanded to \value -> case value of ...
.
The lambda case is provided by the LambdaCase
language extension
Learn more about lambda cases here.
The Pipe operator |>
In IHP code bases you find lots of usage of the |>
operator. We usually call it the pipe operator
.
The operator allows you to write code like this:
user
|> fill @["firstname", "lastname"]
|> validateField #firstname nonEmpty
Instead of the normal function style:
validateField #firstname nonEmpty (
fill @["firstname", "lastname"] user
)
In general:
function arg1 arg2 object
=
object |> function arg1 arg2
The operator itself is defined as a haskell function inside IHP.HaskellSupport
:
infixl 8 |> -- This tells haskell to treat the |> as an infix operator
a |> f = f a -- This is the actual implementation
Tell GHC (Haskell Compiler) To Infer Constraints And Implicit Parameters
Let’s say you are working with a controller ApplicationsAction
and most actions have similar access control:
action NewApplicationAction { jobPositionId } = do
jobPosition <- fetch jobPositionId
-- Access Control
jobPositions <- currentCompanyJobPositions
accessDeniedUnless (jobPosition.id `elem` (ids jobPositions))
...
action UpdateApplicationAction { applicationId } = do
application <- fetch applicationId
-- Access Control
jobPositions <- currentCompanyJobPositions
accessDeniedUnless (jobPosition.id `elem` (ids jobPositions))
...
We could start by refactoring the access control logic into a function:
accessDeniedUnlessJobPositionAllowed jobPosition = do
jobPositions <- currentCompanyJobPositions
accessDeniedUnless (jobPosition.id `elem` (ids jobPositions))
And then add a type declaration:
accessDeniedUnlessJobPositionAllowed :: JobPosition -> IO ()
accessDeniedUnlessJobPositionAllowed jobPosition = do
jobPositions <- currentCompanyJobPositions
accessDeniedUnless (jobPosition.id `elem` (ids jobPositions))
However, GHC will give us an error message stating:
...
Application/Helper/Controller.hs:51:21: error:
* Unbound implicit parameter (?context::ControllerContext)
arising from a use of `currentCompanyJobPositions'
* In a stmt of a 'do' block:
jobPositions <- currentCompanyJobPositions
In the expression:
do jobPositions <- currentCompanyJobPositions
accessDeniedUnless (jobPosition.id `elem` (ids jobPositions))
In an equation for `accessDeniedUnlessJobPositionAllowed':
accessDeniedUnlessJobPositionAllowed jobPosition
= do jobPositions <- currentCompanyJobPositions
accessDeniedUnless (jobPosition.id `elem` (ids jobPositions))
|
51 | jobPositions <- currentCompanyJobPositions
|
We could explicitly add the ?context::ControllerContext
implicit:
accessDeniedUnlessJobPositionAllowed :: (?context::ControllerContext) => JobPosition -> IO ()
Writing out implicit parameters, and other type constrains could become messy and/or irritating, so we could tell GHC to infer it:
accessDeniedUnlessJobPositionAllowed :: _ => JobPosition -> IO ()