Hi, I’m Colin from Canada.
Haskell Developer at Kadena.
@fosskers on Github, Twitter, etc.
Writing FOSS Haskell since 2011.
This presentation uses patat by Jasper Van der Jeugt.
What are we doing?
- The Choice of Haskell
- Fast-feedback Development Environments
- Design Choices (a.k.a. “do you really need an App Monad?”)
- Deploying Haskell Software
…
What ought we (all) do?
- Beauty, Correctness, and Good Technique
- Laws and Principles
- Little-known Idioms / Libraries / Functions
Our technical questions were:
Is there a language that lets us:
- write fast web servers with long uptimes?
- write new languages easily? (i.e. Pact, our smart contract language)
- reuse backend code on the frontend, for a Web REPL? (https://pact.kadena.io/)
…
Our business questions were:
- Will the code be damn correct?
- How much will we have to reinvent the wheel?
…
Our human question was:
- Will the language and its tools fulfill our developers?
chainweb.cabal
file for project definition, default.nix
for CI / deployment.
stack.yaml
and cabal.project
for daily dev.
…
Cabal >= 2.2 (with common
stanzas)
common warning-flags
ghc-options:
-Wall
-Werror
-Wcompat
-Wincomplete-record-updates
-Wincomplete-uni-patterns
-Widentities
-Wpartial-fields
…
chainweb.cabal
has distinct library
and executable
sections.
…
All peripheral binaries are merged into one to reduce link time and binary sizes.
Editor of Choice: Emacs / Spacemacs
Feedback Mechanism: ghcid
Concern | Library |
---|---|
Prelude | Prelude from base |
Testing | tasty + QuickCheck |
Streaming | streaming |
Binary Encoding | cereal + bytes |
Lenses | lens w/ named functions |
Web Server | servant |
Web Client | servant-client |
CLI Flags | configuration-tools |
Logging | yet-another-logger |
Monad Transformer stack? Extensible Effects? RIO?
Plain IO
everywhere. Exceptions are thrown through IO
.
…
What do you auto-generate?
We generate our Lenses with Template Haskell, but hand-write all our Aeson instances.
…
How is your app configured?
Automatic YAML support via configuration-tools
.
We use NixOS, but no nixops
.
CI builds binaries with Nix and caches, remote machines pull from cache.
The machines themselves are managed by Terraform.
Good technique is that which maximizes the harmony between Beauty and Correctness.
For any piece of code, there exists a refactor which approaches a one-liner.
…
Corollary:
If you cannot find the refactor, there is a mistake in your design. Solving the mistake will unlock the refactor.
Instead of writing this…
data PriceHistory = ...
shouldIBuyBitcoin :: Maybe PriceHistory -> IO Bool
…
Write this!
shouldIBuyBitcoin :: PriceHistory -> IO Bool
Instead of writing this…
petTheCat :: [Cat] -> IO ()
petTheCat [] = ... -- Should I throw?
petTheCat (cat:_) = f cat
…
Write this!
import Data.List.NonEmpty (NonEmpty(..))
petTheCat :: NonEmpty Cat -> IO ()
petTheCat (cat :| _) = f cat
…
We can go further with nonempty-containers
, which provides NESet
, NEMap
,
and NESeq
.
class Foldable t => Foldable1 t where
foldMap1 :: Semigroup m => (a -> m) -> t a -> m
Written by Nikita Volkov. Only depends on base
.
{-# LANGUAGE NoImplicitPrelude #-}
import BasePrelude
comeOnSimonWhereIsThis :: Either a b -> Maybe b
…
With errors
by Gabriel Gonzales:
hush :: Either a b -> Maybe b
hushT :: Monad m => ExceptT a m b -> MaybeT m b
note :: a -> Maybe b -> Either a b
noteT :: Monad m => a -> MaybeT m b -> ExceptT a m b
hoistMaybe :: Monad m => Maybe b -> MaybeT m b
hoistEither :: Monad m => Either e a -> ExceptT e m a
failWith :: Applicative m => e -> Maybe a -> ExceptT e m a
Recall these from base
:
mapMaybe :: (a -> Maybe b) -> [a] -> [b]
catMaybes :: [Maybe a] -> [a]
partitionEithers :: [Either a b] -> ([a], [b])
…
From witherable
by Kinoshita Fumiaki:
wither :: Applicative f => (a -> f (Maybe b)) -> t a -> f (t b)
…
From compactable
by Isaac Shapira:
fmapEither :: Functor t => (a -> Either l r) -> t a -> (t l, t r)
traverseEither :: (Applicative f, Traversable t) =>
(a -> f (Either l r)) -> t a -> f (t l, t r)
Recall this from async
:
-- This starts a thread for every item in `t`!
mapConcurrently :: Traversable t => (a -> IO b) -> t a -> IO (t b)
…
From scheduler
by Alexey Kuleschevich:
-- Also exposed are patterns `Par` and `Par'` which automatically use all
-- available cores.
data Comp = Seq | ParOn [Int] | ParN Word16
-- Like `mapConcurrently`, item order is preserved.
traverseConcurrently :: Traversable t => Comp -> (a -> IO b) -> t a -> IO (t b)
Recall a common pattern for generating lenses:
{-# LANGUAGE TemplateHaskell #-}
data Cat = Cat { _name :: Text, _age :: Word, _money :: Double }
makeLenses ''Cat
…
>>> let jack = Cat "Jack" 6 10.0
>>> jack ^. name
"Jack"
>>> jack & money += 5.0
Cat "Jack" 6 15.0
With generic-lens
written by Csongor Kiss:
{-# LANGUAGE DeriveGeneric #-}
data Cat = Cat { name :: Text, age :: Word, money :: Double } deriving (Generic)
…
>>> let jack = Cat "Jack" 6 10.0
>>> jack ^. field @"name"
"Jack"
>>> jack & field @"money" += 5.0
Cat "Jack" 6 15.0
…
But we also get sane ToJSON
and FromJSON
instances this way!
oracle :: Stream (Of Question) IO r -> Stream (Of (Hint, Warning)) IO r
This causes a space leak.
…
With strict-tuple
by Mitchell Rosen:
import Data.Tuple.Strict (T2(..))
oracle :: Stream (Of Question) IO r -> Stream (Of (T2 Hint Warning)) IO r
data JohnsWorkout = JohnsWorkout
{ duration :: Word
, location :: Location
, activities :: [Activity] }
…
{-# LANGUAGE BangPatterns #-}
data JohnsWorkout = JohnsWorkout
{ duration :: {-# UNPACK #-} !Word
, location :: !Location
, activities :: ![Activity] }
This reduces strain on the strictness checker inside GHC.
data Env = Env { _foo :: Foo, ... }
makeLenses ''Env
bar :: Foo -> Foo
…
work :: MonadState Env m => ...
work ... = do
env <- get
put $ env { _foo = bar $ _foo env }
…
work :: MonadState Env m => ...
work ... = modify' (\env -> env { _foo = bar $ _foo env })
…
work :: MonadState Env m => ...
work ... = foo %= bar
getFriend :: IO (Maybe Friend)
sellData :: Friend -> IO (Maybe Cash)
facebookKiller :: IO ()
facebookKiller = do
mfriend <- getFriend
case mfriend of
Nothing -> putStrLn "No!"
Just a -> do
mdata <- sellData a
case mdata of
Nothing -> putStrLn "Zuckerberg wins again..."
Just b -> f b
facebookKiller :: IO ()
facebookKiller = getFriend >>= \case
Nothing -> putStrLn "No!"
Just a -> sellData a >>= \case
Nothing -> putStrLn "Zuckerberg wins again..."
Just b -> f b
…
import Control.Monad.Trans.Maybe (MaybeT(..))
facebookKiller :: IO ()
facebookKiller = runMaybeT g >>= \case
Nothing -> putStrLn "Zuckerberg wins again..."
Just b -> f b
where
g :: MaybeT IO Cash
g = do
friend <- MaybeT getFriend
MaybeT $ sellData friend
import Control.Monad.Trans.Except (ExceptT(..))
facebookKiller :: IO ()
facebookKiller = runExceptT g >>= \case
Left e -> putStrLn e
Right b -> f b
where
g :: ExceptT Text IO Cash
g = do
friend <- noteT "Couldn't find a friend..." $ MaybeT getFriend
noteT "Caught by the government" . MaybeT $ sellData friend
…
facebookKiller :: IO ()
facebookKiller = runExceptT g >>= either putStrLn f
where
...
Written by Gabriel Gonzales.
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeOperators #-}
data ChainwebEnv w = ChainwebEnv
{ nodes :: w ::: Word8 <?> "The number of nodes to simulate"
, config :: w ::: FilePath <?> "Path to config file"
, peer :: w ::: [Text] <?> "Known peers to connect to"
} deriving (Generic)
instance ParseRecord (ChainwebEnv Wrapped)
main :: IO ()
main = do
ChainwebEnv n c p <- unwrapRecord "chainweb"
...
This is another way to adhere to the Principle of Perfect Input.
sendAMuffin :: Text -> Text -> IO ()
sendAMuffin name address = ...
…
newtype Name = Name { name :: Text }
newtype Address = Address { address :: Text }
sendAMuffin :: Name -> Address -> IO ()
sendAMuffin name address = ...
work :: Foo -> IO (Maybe Bar)
solveFamousMathProblem :: Foo -> IO ()
solveFamousMathProblem foo = work foo >>= \mbar ->
case mbar of
Nothing -> ...
Just bar -> ...
…
{-# LANGUAGE LambdaCase #-}
solveFamousMathProblem :: Foo -> IO ()
solveFamousMathProblem foo = work foo >>= \case
Nothing -> ...
Just bar -> ...
…
solveFamousMathProblem :: Foo -> IO ()
solveFamousMathProblem = work >=> traverse_ f
{-# LANGUAGE RecordWildCards #-}
data LaunchTarget = LaunchTarget { _planet :: Planet, _time :: Time, _orbit :: Orbit }
elon :: LaunchTarget -> IO ()
elon LaunchTarget{..} = do
f _planet
g _orbit
...
This does code-gen and slows compiles!
This keeps your Haddocks clean and improves compiler performance.
module Noodles
( -- * Soups
-- | Noodles in some sort of broth.
-- Can be eaten in \(O(\log{n})\) time.
-- ** Chinese
beefNoodle, ramen
-- ** Vietnamese
, pho
-- ** Japanese
, udon, soba
) where
…
Even in your main module:
module Main ( main ) where