F# <$> Haskell: syntax & concepts

#blog#fsharp#haskell

Comapring F# and Haskell syntax and notes on learning the latter.

WIP: This has been mostly scaffolded with AI and I’m fixing any inconsistencies as I learn. Let me know if you spot any mistake.

basics

ConceptF#Haskell
single-line comment// ...-- ...
multi-line comment(* ... *){- ... -}
file/module headermodule M / namespace Nmodule M where
importopen X.Yimport X.Y
entry pointlet main _ = ... (console apps)main :: IO ()
main = ...
indentationsignificant (tabs/spaces)significant (layout rule)

values, functions, application

F#Haskell
valuelet x = 42x = 42
type annotationlet x: int = 42x :: Int
x = 42
functionlet add x y = x + yadd x y = x + y
annotate functionlet add (x:int) (y:int) : int = ...add :: Int -> Int -> Int
add x y = ...
applicationadd 1 2add 1 2
partial applicationlet inc = add 1inc = add 1
compositionlet h = f >> g / let h = g << fh = g . f
forward pipex |> fx & f (needs: import Data.Function ((&)))
backward applyx <| fx $ f (needs: import Data.Function ((&)))
sections(+1); (1+)(1+), (+1)

bindings & scope

F#Haskell
local bindinglet x = ... in exprlet x = ... in expr
where clausesN/Af x = y
where y = ...
recursive bindinglet rec fact n = ...all top-level let are recursive by default

conditionals & pattern matching

F#Haskell
ifif cond then a else bsame
matchmatch v with | A -> ... | B x -> ...case v of A -> ...; B x -> ...
guardsmatch x with | x when x>0 -> ...f x | x>0 = ... (guards on equations)
wildcard__

tuples, lists, arrays, strings

F#Haskell
tuple(1, "a")(1, "a")
list[1;2;3][1,2,3]
cons1 :: [2;3]1 : [2,3]
head/tailList.head xs / List.tail xshead xs / tail xs
array[| 1;2 |]Array via lib; use lists or vector
string"hi" is string"hi" is [Char]; prefer Text
Seq.headSeq.head xshead xs
Seq.tailSeq.tail xstail xs
Seq.lengthSeq.length xslength xs

string manipulation

F#Haskell
split"1;2;3".Split(';')Data.Text.splitOn ";" "1;2;3" (Text)
"a b c".Split(' ')words "a b c" for whitespace (String)
joinString.concat ";" ["1";"2";"3"]Data.Text.intercalate ";" ["1","2","3"] (Text)
unwords ["a","b","c"] for whitespace (String)
trim" hi ".Trim()Data.Text.strip " hi " (Text)
replace"hello".Replace("l", "x")Data.Text.replace "l" "x" "hello" (Text)
length"hello".Lengthlength "hello" (String) / Data.Text.length (Text)
substring"hello".Substring(1, 3)Data.Text.take 3 (Data.Text.drop 1 "hello") (Text)
contains"hello".Contains("ell")Data.Text.isInfixOf "ell" "hello" (Text)
starts/endss.StartsWith("h") / s.EndsWith("o")Data.Text.isPrefixOf / Data.Text.isSuffixOf (Text)
uppercase"hi".ToUpper()Data.Text.toUpper "hi" (Text)
lowercase"HI".ToLower()Data.Text.toLower "HI" (Text)

Note: Haskell’s String is [Char]. For performance, use Data.Text (import Data.Text qualified).

Haskell class / instance vs F# SRTP and interfaces

In Haskell, a type class is like a compile-time interface that defines a set of operations any type can implement. Each concrete implementation is provided via an instance declaration. class / instance system provides compile-time ad-hoc polymorphism, similar to F# SRTP and interfaces.

Haskell example

-- Define a capability (like an interface)
class Show a where
    show :: a -> String

-- Provide an implementation for Int
instance Show Int where
    show = Prelude.show

-- Use it generically: compiler picks the right instance
display :: Show a => a -> String
display x = "Value: " ++ show x

F# example SRTP with instance member

// Define a similar capability via SRTP constraint
let inline display (x: ^a) =
    "Value: " +
    ((^a : (member Show : ^a -> string) x))

// Example type implementing 'Show'-like behavior
type MyType(value: int) =
    member _.Value = value
    member _.Show() = sprintf "MyType(%d)" value

// Works statically like Haskell's typeclass
printfn "%s" (display (MyType(42)))

F# example with interface

type IShow =
    abstract member Show : unit -> string

type MyTypeI(value:int) =
    interface IShow with
        member _.Show() = sprintf "MyTypeI(%d)" value

let displayI (x:#IShow) =
    "Value: " + x.Show()

printfn "%s" (displayI (MyTypeI 42))

ConceptHaskellF# Equivalent
Define capabilityclass Show a where ...SRTP or type IShow
Provide implementationinstance Show Int where ...inline member or interface impl
Use genericallyf :: Show a => a -> Stringlet inline f (x:^a) = ...
ResolutionCompiler picks globallySRTP resolved inline
NatureAd-hoc polymorphismStatic (SRTP) or nominal (interface)
Runtime costNone (dictionary inlined)None for SRTP, vtable for interfaces

records & DUs (ADTs)

F#Haskell
record typetype Person = { Name:string; Age:int }data Person = Person { name :: String, age :: Int }
record value{ Name="A"; Age=1 }Person { name="A", age=1 }
update{ p with Age=2 }p { age = 2 }
union / sumtype Shape = Circle of r:int | Rect of w:int * h:intdata Shape = Circle Int | Rect Int Int
deriving(* auto ToString via printf *)deriving (Show, Eq, Ord)

options, results, eithers

F#Haskell
option/maybeSome 1 / NoneJust 1 / Nothing
result/eitherOk x / Error eRight x / Left e
mapOption.map ffmap f
bindOption.bind f>>= (for Maybe/Either)
defaultOption.defaultValue d optfromMaybe d m

higher-order, folds, comprehensions

F#Haskell
mapList.map f xsmap f xs
filterList.filter p xsfilter p xs
foldList.fold f s xsfoldl' f s xs (strict) / foldr f z xs
list compseq { for x in xs do yield x+1 }[x+1 | x <- xs]

laziness & strictness

F# (strict)Haskell (lazy by default)
lazy valuelazy (exp);default lazy; use seq/deepseq/bang patterns !x for strictness
strict foldList.fold is strict in stateprefer foldl' for large lists

effects & monads

F#Haskell
computation exprasync { ... }, task { ... }do-notation
IOSystem.IO, side effects anywhereIO monad: main :: IO ()
monad opsCE let!, return>>=, >>, pure, return
reader/statecustom CE or librariesReader, State, Except (mtl, transformers)
async/concurrencyAsync, Taskasync, STM (atomically, TVar)

small monad example

F# (option CE):

let addParsed a b =
  option {
    let! x = System.Int32.TryParse a |> Option.ofTry
    let! y = System.Int32.TryParse b |> Option.ofTry
    return x + y
  }

Haskell (Maybe + do):

readInt :: String -> Maybe Int
readInt s = case reads s of [(n,"")] -> Just n; _ -> Nothing

addParsed :: String -> String -> Maybe Int
addParsed a b = do
  x <- readInt a
  y <- readInt b
  pure (x + y)

typeclasses vs interfaces

F#Haskell
ad-hoc polymorphisminterfaces, SRTPstypeclasses
defineinterface + implsclass Show a where show :: a -> String
implementtype T with interface ...instance Show T where show ...
usemethod dispatchconstraints: foo :: Show a => a -> String

newtype & type aliases

F#Haskell
aliastype OrderId = stringtype OrderId = String (alias)
zero-cost wrapoften single-case DUnewtype OrderId = OrderId String (no runtime cost)

numeric & overloading

F#Haskell
operator overloadinglimited / SRTPsvia typeclasses (Num, Fractional)
from int/floatcastsfromIntegral, realToFrac

errors & exceptions

F#Haskell
exceptionstry ... with ex -> ...catch in IO; prefer Either/ExceptT in pure code
fail fastthrowerror "msg" (avoid in production)

modules & names

F#Haskell
modulemodule M or module recmodule M (exports) where
open/importopen Ximport X (..) / qualified
qualifiedopen type/aliasesimport qualified M as X then X.foo

deriving & generics

F#Haskell
auto eq/showstructural eq, ToStringderiving (Eq, Ord, Show, Generic)
generic prog.SRTPs, MemberConstraintGHC.Generics, DerivingVia, DeriveAnyClass

quick JSON example

F# (System.Text.Json)

type Person = { name: string; age: int }
let json = JsonSerializer.Serialize { name="Ada"; age=37 }

Haskell (aeson) Enable GHC language extensions for generics with {-# LANGUAGE DeriveGeneric #-}

{-# LANGUAGE DeriveGeneric #-}
import Data.Aeson
import GHC.Generics (Generic)

data Person = Person { name :: String, age :: Int }
  deriving (Show, Generic)

instance FromJSON Person
instance ToJSON   Person

json = encode (Person "Ada" 37)

IO & files

F#

let lines = System.IO.File.ReadAllLines "a.txt"

Haskell

import qualified Data.Text.IO as T
txt <- T.readFile "a.txt"

command-line args

F#

let args = System.Environment.GetCommandLineArgs()
// or in main:
[<EntryPoint>]
let main argv =
  argv |> Array.iter (printfn "Arg: %s")
  0

Haskell

import System.Environment (getArgs)

main :: IO ()
main = do
  getArgs >>= mapM_ (\arg -> putStrLn $ "Arg: " ++ arg)

F# (with FSharpPlus) vs Haskell - Effects & Monads Cheatsheet

Imports

F#:

  • nuget: FSharpPlus
  • open FSharpPlus
  • open FSharpPlus.Data

Haskell:

  • base + transformers + mtl + containers (as needed)

Functor / Applicative / Monad

F#:

open FSharpPlus
let plus1 = map ((+) 1) (Some 1)        // Functor: map / <!>
let apEx  = apply (Some (+3)) (Some 4)  // Applicative: apply / <*>
let bindEx = Option.bind (fun x -> Some (x*2)) (Some 3) // Monad: bind / >>=

Haskell:

fmap ((+) 1) (Just 1)
(Just (+3)) <*> (Just 4)
Just 3 >>= \x -> Just (x*2)

Standard Aliases

F#:

  • map == <!>
  • apply == <*>
  • bind == >>= (from F#+)

Haskell:

  • fmap == <$>
  • (<*>) Applicative apply
  • (>>=) Monad bind

Maybe / Option

F#:

open FSharpPlus
let parseInt (s:string) =
  match System.Int32.TryParse s with true, n -> Some n | _ -> None

let addParsed a b =
  maybe {
    let! x = parseInt a      // maybe CE is in F#+
    let! y = parseInt b
    return x + y
  }

Haskell:

readInt s = case reads s of [(n,"")] -> Just n; _ -> Nothing
addParsed a b = do x <- readInt a; y <- readInt b; pure (x + y)

Result / Either

F#:

open FSharpPlus
type Err = string
let divide x y = if y=0 then Error "div by zero" else Ok (x / y)

let calc a b =
  result {
    let! x = divide a b
    return x * 10
  }

Haskell (Either String):

divide x y = if y==0 then Left "div by zero" else Right (x `div` y)
calc a b = do x <- divide a b; pure (x * 10)

Reader

F#:

open FSharpPlus.Data
type Cfg = { factor:int }

let multByCfg : Reader<Cfg,int> =
  reader {
    let! cfg = ask ()               // ask :: Reader<Cfg, Cfg>
    return cfg.factor * 3
  }
// runReader multByCfg { factor = 7 }  // -> 21

Haskell:

newtype Reader r a = Reader { runReader :: r -> a }
multByCfg = do cfg <- ask; pure (factor cfg * 3)
runReader multByCfg (Cfg 7)

State

F#:

open FSharpPlus.Data
let inc : State<int,unit> =
  state {
    let! n = get ()
    do! put (n+1)
  }
// runState inc 10  // -> ((), 11)

Haskell:

inc = do n <- get; put (n+1)

Writer (logging)

F#:

open FSharpPlus.Data
let step : Writer<List<string>, int> =
  writer {
    do! tell ["start"]
    return 42
  }
// runWriter step  // -> (42, ["start"])

Haskell:

step = do tell ["start"]; pure 42

Monad stacks (Reader + State + Result/Either)

F# with F#+ transformers:

type App<'a> = ReaderT<Cfg, StateT<int, ResultT<Err, 'a>>>

let runApp (m: App<'a>) cfg st =
  m |> ReaderT.run cfg |> StateT.run st |> ResultT.run

let program : App<int> =
  monad {
    let! cfg = ReaderT.ask ()
    do! StateT.modify ((+) 1)
    if cfg.factor < 0 then
       return! ResultT.throw "bad cfg"
    let! s = StateT.get ()
    return cfg.factor + s
  }
// runApp program { factor = 2 } 10  -> Ok (13, 11)

Haskell (mtl):

type App a = ReaderT Cfg (StateT Int (ExceptT Err IO)) a   -- or without IO
program = do
  cfg <- ask
  modify (+1)
  when (factor cfg < 0) (throwError "bad cfg")
  s <- get
  pure (factor cfg + s)

Traversable / sequence / traverse

F# (F#+):

let parsed : Option<int list> = traverse parseInt ["1";"2";"x"]  // -> None
let lifted : list<option<int>> = sequence [Some 1; None; Some 3]  // -> None overall

Haskell:

traverse readInt ["1","2","x"]   -- :: Maybe [Int]
sequence [Just 1, Nothing, Just 3]

Note: F#+ traverse/sequence are generic over many containers, like Haskell.

Alternative / Plus (choice, failure)

F#:

open FSharpPlus.Control
let pickFirst : option<int> = (None <|> Some 42)              // <|> from Alternative
let guarded = guard (5 > 3) *> Some "ok"                      // guard + *> in F#+

Haskell:

pickFirst = Nothing <|> Just 42
guarded   = guard (5 > 3) *> Just "ok"

Validation (Applicative accumulation)

F# (F#+ Validation accumulates errors Applicatively):

open FSharpPlus.Data
let validatePositive x = if x > 0 then Success x else Failure ["not positive"]
let validateSmall   x = if x < 10 then Success x else Failure ["too large"]

let combined =
  let (<*>) = Validation.apply   // ensure Applicative
  Validation.map2 (fun a b -> a + b) (validatePositive 5) (validateSmall 12)
// -> Failure ["too large"]

Haskell (Data.Validation from ‘validation’ pkg):

validatePositive x = if x>0 then Success x else Failure ["not positive"]
validateSmall   x = if x<10 then Success x else Failure ["too large"]
combined = (+) <$> validatePositive 5 <*> validateSmall 12
-- Failure ["too large"]

Async / IO

F#:

// Async: built-in; Task: .NET
let ioExample =
  async {
    do! Async.Sleep 10
    return 123
  }

Haskell:

import Control.Concurrent (threadDelay)
ioExample = do threadDelay 10000; pure 123 :: IO Int

Lifting / Hoisting helpers

F# (F#+):

let liftReader (x: Reader<Cfg,int>) : ReaderT<Cfg, Result<int, _>> =
  ReaderT (fun r -> Ok (Reader.run x r))

Haskell:

-- liftReader = hoist (withReaderT ...) or use 'lift' when inside a transformer stack

Common Operators Summary

F# (F#+)Haskell
functor map<!><$>
applicative apply<*><*>
alternative choice`<>`
flipped map<&><&>
bind>>=>>= , =<<
sequence (keep R/L)*> , <**> , <*
forward pipe|>& (Data.Function)
traverse / sequencetraverse / sequencetraverse / sequence / traverse_
reader opsask, local, reader {..}ask, local (Reader)
state opsstate {..}, get/put/modifyget/put/modify (State)
writer opswriter {..}, tell/listentell (Writer)
monad transformersResultT/OptionT/ReaderT/StateT/WriterTExceptT/MaybeT/ReaderT/StateT/WriterT

tips moving F# → Haskell

  • Laziness by default: watch space leaks; use seq, deepseq, foldl', bang patterns.
  • Typeclasses everywhere: numeric ops, equality, printing are constrained (Eq, Show, Num).
  • All top-level bindings are recursive; use where/let elegantly.
  • Lists are linked lists; reach for Vector/Text/ByteString for perf.
  • newtype is your friend for domain safety with zero overhead.

TODO: Haskell Advanced features:

`GADTs`, `DataKinds`, `TypeFamilies`, `GeneralizedNewtypeDeriving`.

Looking for a consultant, collaborator, or dev-for-hire? I'm open to consulting, creative tech collaborations, and new gigs.

Let's Talk