F# <$> Haskell: syntax & concepts
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
Concept | F# | Haskell |
---|---|---|
single-line comment | // ... | -- ... |
multi-line comment | (* ... *) | {- ... -} |
file/module header | module M / namespace N | module M where |
import | open X.Y | import X.Y |
entry point | let main _ = ... (console apps) | main :: IO () main = ... |
indentation | significant (tabs/spaces) | significant (layout rule) |
values, functions, application
F# | Haskell | |
---|---|---|
value | let x = 42 | x = 42 |
type annotation | let x: int = 42 | x :: Int x = 42 |
function | let add x y = x + y | add x y = x + y |
annotate function | let add (x:int) (y:int) : int = ... | add :: Int -> Int -> Int add x y = ... |
application | add 1 2 | add 1 2 |
partial application | let inc = add 1 | inc = add 1 |
composition | let h = f >> g / let h = g << f | h = g . f |
forward pipe | x |> f | x & f (needs: import Data.Function ((&))) |
backward apply | x <| f | x $ f (needs: import Data.Function ((&))) |
sections | (+1) ; (1+) | (1+) , (+1) |
bindings & scope
F# | Haskell | |
---|---|---|
local binding | let x = ... in expr | let x = ... in expr |
where clauses | N/A | f x = y where y = ... |
recursive binding | let rec fact n = ... | all top-level let are recursive by default |
conditionals & pattern matching
F# | Haskell | |
---|---|---|
if | if cond then a else b | same |
match | match v with | A -> ... | B x -> ... | case v of A -> ...; B x -> ... |
guards | match 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] |
cons | 1 :: [2;3] | 1 : [2,3] |
head/tail | List.head xs / List.tail xs | head xs / tail xs |
array | [| 1;2 |] | Array via lib; use lists or vector |
string | "hi" is string | "hi" is [Char] ; prefer Text |
Seq.head | Seq.head xs | head xs |
Seq.tail | Seq.tail xs | tail xs |
Seq.length | Seq.length xs | length 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) | |
join | String.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".Length | length "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/ends | s.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, useData.Text
(importData.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))
Concept | Haskell | F# Equivalent |
---|---|---|
Define capability | class Show a where ... | SRTP or type IShow |
Provide implementation | instance Show Int where ... | inline member or interface impl |
Use generically | f :: Show a => a -> String | let inline f (x:^a) = ... |
Resolution | Compiler picks globally | SRTP resolved inline |
Nature | Ad-hoc polymorphism | Static (SRTP) or nominal (interface) |
Runtime cost | None (dictionary inlined) | None for SRTP, vtable for interfaces |
records & DUs (ADTs)
F# | Haskell | |
---|---|---|
record type | type 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 / sum | type Shape = Circle of r:int | Rect of w:int * h:int | data Shape = Circle Int | Rect Int Int |
deriving | (* auto ToString via printf *) | deriving (Show, Eq, Ord) |
options, results, eithers
F# | Haskell | |
---|---|---|
option/maybe | Some 1 / None | Just 1 / Nothing |
result/either | Ok x / Error e | Right x / Left e |
map | Option.map f | fmap f |
bind | Option.bind f | >>= (for Maybe /Either ) |
default | Option.defaultValue d opt | fromMaybe d m |
higher-order, folds, comprehensions
F# | Haskell | |
---|---|---|
map | List.map f xs | map f xs |
filter | List.filter p xs | filter p xs |
fold | List.fold f s xs | foldl' f s xs (strict) / foldr f z xs |
list comp | seq { for x in xs do yield x+1 } | [x+1 | x <- xs] |
laziness & strictness
F# (strict) | Haskell (lazy by default) | |
---|---|---|
lazy value | lazy (exp) ; | default lazy; use seq /deepseq /bang patterns !x for strictness |
strict fold | List.fold is strict in state | prefer foldl' for large lists |
effects & monads
F# | Haskell | |
---|---|---|
computation expr | async { ... } , task { ... } | do -notation |
IO | System.IO , side effects anywhere | IO monad: main :: IO () |
monad ops | CE let! , return | >>= , >> , pure , return |
reader/state | custom CE or libraries | Reader , State , Except (mtl , transformers ) |
async/concurrency | Async , Task | async , 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 polymorphism | interfaces, SRTPs | typeclasses |
define | interface + impls | class Show a where show :: a -> String |
implement | type T with interface ... | instance Show T where show ... |
use | method dispatch | constraints: foo :: Show a => a -> String |
newtype & type aliases
F# | Haskell | |
---|---|---|
alias | type OrderId = string | type OrderId = String (alias) |
zero-cost wrap | often single-case DU | newtype OrderId = OrderId String (no runtime cost) |
numeric & overloading
F# | Haskell | |
---|---|---|
operator overloading | limited / SRTPs | via typeclasses (Num , Fractional ) |
from int/float | casts | fromIntegral , realToFrac |
errors & exceptions
F# | Haskell | |
---|---|---|
exceptions | try ... with ex -> ... | catch in IO ; prefer Either/ExceptT in pure code |
fail fast | throw | error "msg" (avoid in production) |
modules & names
F# | Haskell | |
---|---|---|
module | module M or module rec | module M (exports) where |
open/import | open X | import X (..) / qualified |
qualified | open type /aliases | import qualified M as X then X.foo |
deriving & generics
F# | Haskell | |
---|---|---|
auto eq/show | structural eq, ToString | deriving (Eq, Ord, Show, Generic) |
generic prog. | SRTPs, MemberConstraint | GHC.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 / sequence | traverse / sequence | traverse / sequence / traverse_ |
reader ops | ask , local , reader {..} | ask , local (Reader) |
state ops | state {..} , get /put /modify | get /put /modify (State) |
writer ops | writer {..} , tell /listen | tell (Writer) |
monad transformers | ResultT /OptionT /ReaderT /StateT /WriterT | ExceptT /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`.