Editor Tooling ( Guest talk @ Sentenai )

Stephen Diehl ( @smdiehl )

May 24, 2016

ghci

GHCi is the interactive shell for the GHC compiler. GHCi is where we will spend most of our time in every day development.

Command Shortcut Action
:reload :r Code reload
:type :t Type inspection
:kind :k Kind inspection
:info :i Information
:print :p Print the expression
:edit :e Load file in system editor
:load :l Set the active Main module in the REPL
:add :ad Load a file into the REPL namespace
:browse :bro Browse all available symbols in the REPL namespace

Cool Prompts

:set prompt "λ: "
:set prompt "ΠΣ: "

Little Known GHCi tricks

Shell Commands

:def make (\_ -> return ":! make")
:def spiritanimal (\_ -> return ":! cowsay -f tux \"More types!\" ")

Inline Hoogle

:def hoogle \s -> return $ ":! hoogle --count=15 \"" ++ s ++ "\""
λ: :hoogle (a -> b) -> f a -> f b
Data.Traversable fmapDefault :: Traversable t => (a -> b) -> t a -> t b
Prelude fmap :: Functor f => (a -> b) -> f a -> f b

Profiling

λ: :set +s
λ: foldr (+) 0 [1..25]
325
it :: Prelude.Integer
(0.02 secs, 4900952 bytes)

Preloaded Custom Prelude

The Prelude isn't great, but GHC is wonderful in that it allows us to replace it with your company's industrial Prelude's (protolude, classy-prelude, Sentenai.Prelude, etc).

$ ghci -package classy-prelude -XNoImplicitPrelude
$ stack ghci --package protolude --ghc-options -XNoImplicitPrelude

Inline Documentation

Several commands can be added to your .ghci config to lookup module information.

:def browser \ u -> return $ ":! chromium-browser " ++ u
:def package \ m -> return $ ":! ghc-pkg --simple-output find-module " ++ m
:{
:def doc \ m -> return $ if m == ""
                         then ":browser $(ghc --print-libdir)/../../share/doc/ghc/html/index.html"
                         else ":browser $(ghc-pkg --simple-output field $(ghc-pkg --simple-output find-module " ++ m ++ ") haddock-html)/$(echo " ++ m ++ " | tr . -).html"
:}
λ> :package Control.Monad.Reader
mtl-2.2.1
λ> :package Type
ghc-7.10.3
λ> :package Data.Text
text-1.2.2.0

Compile your sandbox with --enable-documentation.

λ> :doc Type
λ> :doc Data.Text

GHCi is slow

Performance in GHCi not representative of code compiled with -O2 and in many cases it can be order of magnitudes slower and even diverge when the compiled code would optimize allocations.

For large projects, GHCi with the default flags can use quite a bit of memory and take a long time to compile. To speed compilation by keeping artificats for compiled modules around, we can enable object code compilation instead of bytecode.

:set -fobject-code

Enabling object code compliation may complicate type inference, since type information provided to the shell can sometimes be less informative than source-loaded code. This under specificity can result in breakage with some langauge extensions. In that case, you can temporarily reenable bytecode compilation on a per module basis with the -fbyte-code flag.

:set -fbyte-code
:load MyModule.hs

Don't compile anything, just typecheck.

:set -fno-code

Design code to be loadable in GHCi

Strive to have your code loadable into GHCi, it helps to iterate quickly and let others figure out how to use your code. If you can't load your code into GHCi it makes it harder to onboard developers.

What can break ghci?

Dumping Derived Instances

Deriving isn't magic, it's just mechanical. Sometimes adding -dsuppress-module-prefixes will help clear this up.

λ> :set -ddump-derived
λ> data Pie = Apple | Pumpkin deriving (Show, Generic)

==================== Derived instances ====================
Derived instances:
  instance Show Pie where
    showsPrec _ Apple = showString "Apple"
    showsPrec _ Pumpkin = showString "Pumpkin"
    showList = showList__ (showsPrec 0)
  
  instance Generic Pie where
    from Apple = M1 (L1 (M1 U1))
    from Pumpkin = M1 (R1 (M1 U1))
    to (M1 (L1 (M1 U1))) = Apple
    to (M1 (R1 (M1 U1))) = Pumpkin
  
  instance Datatype D1Pie where
    datatypeName _ = "Pie"
    moduleName _ = "Ghci5"
  
  instance Constructor C1_0Pie where conName _ = "Apple"
  
  instance Constructor C1_1Pie where conName _ = "Pumpkin"
  

Generic representation:
  
  Generated datatypes for meta-information:
    D1Pie
    C1_0Pie
    C1_1Pie
  
  Representation types:
    type Rep Pie = D1 D1Pie (C1 C1_0Pie U1 :+: C1 C1_1Pie U1)

Introspecting Generics

Suppose we wanted to write generic substitutions that we can automatically have GHC write for (typing environment, complex types containing Type, containers, etc). We could write the boilerplate, or we could use GHC.Generics.

-- | Type substitution over type variables.
newtype Subst = Subst (Map.Map Id Type)
  deriving (Eq, Ord, Show, Generic)

data Type
  = TVar Var
  | TCon Var
  | TApp Type Type
  | TForall [Pred] [Var] Type
  deriving (Show, Eq, Ord, Generic)
class Types a where
  -- | Apply a type substitution.
  apply :: Subst -> a -> a

  default apply :: (Generic a, GTypes (Rep a)) => Subst -> a -> a
  apply su a = to (gapply su (from a))

instance Types Type where
  apply s (TApp a b) = TApp (apply s a) (apply s b)
  apply _ t@TCon{}   = t
  apply s t@(TVar p) = Map.findWithDefault t p (sMap s)

instance Types Id where
  apply s x = x

Using GHCi we can inspect the structure of the associated data type Rep from the derived Generic class.

λ> import GHC.Generics 
λ> :kind! Rep Type
Rep Type :: * -> *
= D1
    Types.Type.D1Type
    ((C1 Types.Type.C1_0Type (S1 NoSelector (Rec0 Var))
      :+: C1 Types.Type.C1_1Type (S1 NoSelector (Rec0 Var)))
     :+: (C1
            Types.Type.C1_2Type
            (S1 NoSelector (Rec0 Type) :*: S1 NoSelector (Rec0 Type))
          :+: (C1
                 Types.Type.C1_3Type
                 (S1 NoSelector (Rec0 Type) :*: S1 NoSelector (Rec0 Type))
               :+: C1
                     Types.Type.C1_4Type
                     (S1 NoSelector (Rec0 [Pred])
                      :*: (S1 NoSelector (Rec0 [Var]) :*: S1 NoSelector (Rec0 Type))))))
type Par0 = K1 P
type Rec0 = K1 R
type S1 = M1 S
type C1 = M1 C
type D1 = M1 D
class GTypes f where
  gapply :: Subst -> f a -> f a

instance Types c => GTypes (K1 i c) where
  gapply su (K1 c) = K1 (apply su c)

instance GTypes f => GTypes (M1 i c f) where
  gapply su (M1 fp) = M1 (gapply su fp)

instance GTypes U1 where
  gapply _ u = u

instance (GTypes f, GTypes g) => GTypes (f :+: g) where
  gapply su (L1 fp) = L1 (gapply su fp)
  gapply su (R1 fp) = R1 (gapply su fp)

instance (GTypes f, GTypes g) => GTypes (f :*: g) where
  gapply su (fp :*: gp) = gapply su fp :*: gapply su gp

We get instances of this for free now!

instance Types Pred where
instance Types a => Types [a] where
instance (Types a, Types b) => Types (a,b) where
instance Types a => Types (Located a) where
  apply su (Located l a) = Located l (apply su a)

ghcid

ghcid is a lightweight IDE hook that allows continuous feedback whenever code is updated. It is run from the command line in the root of the cabal project directory by specifying a command to run (e.g., ghci, cabal repl, or stack repl).

ghcid --command="cabal repl"   # Run cabal repl under ghcid
ghcid --command="stack repl"   # Run stack repl under ghcid
ghcid --command="ghci baz.hs"  # Open baz.hs under ghcid

Vim Integration

Vim has wonderful support for Haskell using ghc-mod and syntastic. NeoVim has made a lot of progress in the last couple of months with haskell-vim.

See: Vim And Haskell in 2016

map <Leader>s :SyntasticToggleMode<CR>

set statusline+=%#warningmsg#
set statusline+=%{SyntasticStatuslineFlag()}
set statusline+=%*

let g:syntastic_always_populate_loc_list = 1
let g:syntastic_auto_loc_list = 0
let g:syntastic_check_on_open = 0
let g:syntastic_check_on_wq = 0
map <silent> tw :GhcModTypeInsert<CR>
map <silent> ts :GhcModSplitFunCase<CR>
map <silent> tq :GhcModType<CR>
map <silent> te :GhcModTypeClear<CR>

Tab Completion

let g:SuperTabDefaultCompletionType = '<c-x><c-o>'

if has("gui_running")
  imap <c-space> <c-r>=SuperTabAlternateCompletion("\<lt>c-x>\<lt>c-o>")<cr>
else " no gui
  if has("unix")
    inoremap <Nul> <c-r>=SuperTabAlternateCompletion("\<lt>c-x>\<lt>c-o>")<cr>
  endif
endif

Debugging GHC Internals

GHC's default Outputtable form is kind of gnarly, we can tame it a bit with some suppressions.

In your .bashrc or .zshrc file

alias ghci-core="ghci -ddump-simpl -dsuppress-idinfo \
-dsuppress-coercions -dsuppress-type-applications \
-dsuppress-uniques -dsuppress-module-prefixes"
λ> let fibs = 0 : zipWith (+) (1 : fibs) fibs

==================== Simplified expression ====================
GHC.Base.returnIO
  @ [()]
  (GHC.Types.:
     @ ()
     ((\ (@ a_ayL) ($dNum_ayM :: GHC.Num.Num a_ayL) ->
         let {
           a_syZ :: a_ayL
           [LclId,
            Str=DmdType,
            Unf=Unf{Src=<vanilla>, TopLvl=False, Value=False, ConLike=False,
                    WorkFree=False, Expandable=False, Guidance=IF_ARGS [] 130 0}]
           a_syZ = GHC.Num.fromInteger @ a_ayL $dNum_ayM (__integer 0) } in
         letrec {
           fibs_apE :: [a_ayL]
           [LclId,
            Str=DmdType,
            Unf=Unf{Src=<vanilla>, TopLvl=False, Value=True, ConLike=True,
                    WorkFree=True, Expandable=True, Guidance=IF_ARGS [] 10 30}]
           fibs_apE = GHC.Types.: @ a_ayL a_syZ a_sz0;
           a_sz0 [Occ=LoopBreaker] :: [a_ayL]
           [LclId,
            Str=DmdType,
            Unf=Unf{Src=<vanilla>, TopLvl=False, Value=False, ConLike=False,
                    WorkFree=False, Expandable=False, Guidance=IF_ARGS [] 200 0}]
           a_sz0 =
             GHC.List.zipWith
               @ a_ayL
               @ a_ayL
               @ a_ayL
               (GHC.Num.+ @ a_ayL $dNum_ayM)
               (GHC.Types.:
                  @ a_ayL
                  (GHC.Num.fromInteger @ a_ayL $dNum_ayM (__integer 1))
                  fibs_apE)
               fibs_apE; } in
         fibs_apE)
      `cast` (UnivCo mkUnsafeCo representational
                (forall a_ayI. GHC.Num.Num a_ayI => [a_ayI]) ()
              :: (forall a_ayI. GHC.Num.Num a_ayI => [a_ayI]) ~R# ()))
     (GHC.Types.[] @ ()))

Becomes this:

λ> let fibs = 0 : zipWith (+) (1 : fibs) fibs

==================== Simplified expression ====================
returnIO
  (: (let {
        a :: Int
        a = I# 0 } in
      letrec {
        fibs :: [Int]
        fibs = : a a;
        a :: [Int]
        a = zipWith (+ $fNumInt) (: (I# 1) fibs) fibs; } in
      fibs `cast` ...)
     ([]))

Custom Type Errors

DSL — A domain specific language, where code is written in one language and errors are given in another.

EDSL — A embedded domain specific language, where code is written in one language and errors are given in the errors of the encoding between the host language. Synonyms: suffering, pain, insanity

Not anymore though! New feature in GHC 8.0. Going to be big.

GHC Documentation

data ErrorMessage where
  Text :: Symbol -> ErrorMessage
  ShowType :: t -> ErrorMessage

  -- Put two messages next to each other
  (:<>:) :: ErrorMessage -> ErrorMessage -> ErrorMessage

  -- Put two messages on top of each other
  (:$$:) :: ErrorMessage -> ErrorMessage -> ErrorMessage
{-# LANGUAGE GADTs #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-} -- NOOOOOO

import GHC.TypeLits

type family Coerce a b where
  Coerce Int Int     = Int
  Coerce Float Float = Float
  Coerce Int Float   = Float
  Coerce Float Int   = TypeError (Text "Cannot cast to smaller type")

data Expr a where
  EInt    :: Int -> Expr Int
  EFloat  :: Float -> Expr Float
  ECoerce :: Expr b -> Expr c -> Expr (Coerce b c)

foo :: Expr Int
foo = ECoerce (EFloat 3) (EInt 4)
λ> :load error_dsl.hs 
[1 of 1] Compiling Main             ( error_dsl.hs, interpreted )

error_dsl.hs:21:7: error:Cannot cast to smaller typeIn the expression: ECoerce (EFloat 3) (EInt 4)
      In an equation for ‘foo’: foo = ECoerce (EFloat 3) (EInt 4)
Failed, modules loaded: none.

Custom HLint Rules

Little known fact that hlint will check for a HLint.hs file in your project and allow it to supply custom hint rules that integrate with your editor. These are specified in it's own rule language. Good for enforcing company-wide style around functions.

Example Rules

For example:

ignore "Avoid Lambda"
ignore "Eta reduce"

error "generalize fmap"  = map  ==> fmap

-- AMP fallout
error "generalize mapM"  = mapM  ==> traverse
error "generalize mapM_" = mapM_ ==> traverse_
error "generalize forM"  = forM  ==> for
error "generalize forM_" = forM_ ==> for_
error "Avoid return" =
    return ==> pure
    where note = "return is obsolete as of GHC 7.10"

Type Holes

I don't personally use them that much ( ghc-mod is more interactive ) but they are useful sometime.

{-# LANGUAGE NoImplicitPrelude #-}

class Functor f where
  fmap :: (a -> b) -> f a -> f b  

instance Functor [] where
  fmap f (x:xs) = f x : fmap f _
sample.hs:7:32:
    Found hole ‘_’ with type: [a]
    Where: ‘a’ is a rigid type variable bound by
               the type signature for fmap :: (a -> b) -> [a] -> [b]
               at sample.hs:7:3
    Relevant bindings include
      xs :: [a] (bound at sample.hs:7:13)
      x :: a (bound at sample.hs:7:11)
      f :: a -> b (bound at sample.hs:7:8)

Can put type holes inside the function context to infer constraints, and you can name the holes. Enabling -XPartialTypeSignatures will allow the compilation to continue if the program is well-typed under inference ( it still needs to be able to resolve dictionaries! ).

{-# LANGUAGE PartialTypeSignatures #-}

foo :: (Show _a, _) => _a -> _
foo x = show (succ x)

Warnings, not errors. Compilation proceeds.

sample2.hs:3:18: Warning:
    Found hole ‘_’ with inferred constraints: (Enum _a)
    In the type signature for ‘foo’: (Show _a, _) => _a -> _

sample2.hs:3:30: Warning:
    Found hole ‘_’ with type: String
    In the type signature for ‘foo’: (Show _a, _) => _a -> _