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 :: Intx = 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 -> Intadd 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
Stringis[Char]. For performance, useData.Text(importData.Textqualified).
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 FSharpPlusopen 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/letelegantly. - Lists are linked lists; reach for
Vector/Text/ByteStringfor perf. newtypeis your friend for domain safety with zero overhead.
TODO: Haskell Advanced features:
`GADTs`, `DataKinds`, `TypeFamilies`, `GeneralizedNewtypeDeriving`.