Agent Skills: Convert F# to Haskell

Bidirectional conversion between Fsharp and Haskell. Use when migrating projects between these languages in either direction. Extends meta-convert-dev with Fsharp↔Haskell specific patterns.

UncategorizedID: arustydev/ai/convert-fsharp-haskell

Repository

aRustyDevLicense: AGPL-3.0
72

Install this agent skill to your local

pnpm dlx add-skill https://github.com/aRustyDev/agents/tree/HEAD/content/skills/convert-fsharp-haskell

Skill Files

Browse the full folder contents for convert-fsharp-haskell.

Download Skill

Loading file tree…

content/skills/convert-fsharp-haskell/SKILL.md

Skill Metadata

Name
convert-fsharp-haskell
Description
Bidirectional conversion between Fsharp and Haskell. Use when migrating projects between these languages in either direction. Extends meta-convert-dev with Fsharp↔Haskell specific patterns.

Convert F# to Haskell

Convert F# code to idiomatic Haskell. This skill extends meta-convert-dev with F#-to-Haskell specific type mappings, idiom translations, and tooling for translating from .NET functional-first to pure functional programming.

This Skill Extends

  • meta-convert-dev - Foundational conversion patterns (APTV workflow, testing strategies)

For general concepts like the Analyze → Plan → Transform → Validate workflow, testing strategies, and common pitfalls, see the meta-skill first.

This Skill Adds

  • Type mappings: F# discriminated unions → Haskell algebraic data types
  • Idiom translations: Computation expressions → do-notation and monads
  • Error handling: Result/Option → Either/Maybe with standard monadic patterns
  • Async patterns: F# async workflows → IO monad and async library
  • Type providers: F# compile-time generation → Haskell Template Haskell or runtime parsing
  • Purity: F# impure-by-default → Haskell pure-by-default with explicit IO

This Skill Does NOT Cover

  • General conversion methodology - see meta-convert-dev
  • F# language fundamentals - see lang-fsharp-dev
  • Haskell language fundamentals - see lang-haskell-dev
  • .NET interop specifics - focus on pure functional patterns

Quick Reference

| F# | Haskell | Notes | |-----|---------|-------| | type Person = { Name: string } | data Person = Person { name :: String } | Record types | | type Shape = Circle of float \| Square of float | data Shape = Circle Float \| Square Float | Discriminated unions → ADTs | | Option<'T> | Maybe a | Direct correspondence | | Result<'T,'E> | Either e a | Note: error on left, value on right | | List<'T> / 'T list | [a] | Lists | | async { ... } | do { ... } in IO monad | Async workflow → IO actions | | let! / do! | <- in do-notation | Monadic bind | | \|> (pipe forward) | $ or & (application) | Function application | | >> (compose forward) | . (compose backward) | Function composition (reversed!) | | [<Measure>] type kg | Phantom types or units library | Units of measure |


When Converting Code

  1. Analyze source thoroughly before writing target - understand computation expressions and their translations
  2. Map types first - create type equivalence table for domain models
  3. Identify purity boundaries - F# allows side effects anywhere, Haskell requires IO monad
  4. Adopt target idioms - embrace Haskell's type classes (Functor, Applicative, Monad)
  5. Handle edge cases - null safety, lazy evaluation, bottom values
  6. Test equivalence - same inputs → same outputs
  7. Leverage type system - use Haskell's type classes to reduce boilerplate

Type System Mapping

Primitive Types

| F# | Haskell | Notes | |-----|---------|-------| | string | String or Text | Use Text for performance, String for compatibility | | int | Int (machine int) or Integer (arbitrary precision) | F# int is Int32, Haskell Int is machine-word | | int64 | Int64 or Integer | Explicit 64-bit | | float / double | Double | IEEE 754 double precision | | decimal | Rational or Scientific | Exact decimal arithmetic | | bool | Bool | Direct mapping | | char | Char | Direct mapping | | unit | () | Unit type | | byte | Word8 | Unsigned 8-bit |

Collection Types

| F# | Haskell | Notes | |-----|---------|-------| | 'T list | [a] | Linked lists (similar performance) | | 'T array | Array a (Data.Array) or Vector a | Immutable arrays; Vector for performance | | List<'T> (.NET) | [a] or Seq a | .NET List → Haskell list or sequence | | Set<'T> | Set a (Data.Set) | Ordered sets | | Map<'K,'V> | Map k v (Data.Map) | Ordered maps | | 'T seq | [a] (lazy evaluation by default) | Sequences → lazy lists | | ('A * 'B) | (a, b) | Tuples (direct mapping) | | ('A * 'B * 'C) | (a, b, c) | Multi-element tuples |

Composite Types

| F# | Haskell | Notes | |-----|---------|-------| | type alias Person = { ... } | data Person = Person { ... } | Records | | type Shape = Circle \| Square | data Shape = Circle Float \| Square Float | Discriminated unions → ADTs | | Option<'T> | Maybe a | Some xJust x, NoneNothing | | Result<'T,'E> | Either e a | Ok xRight x, Error eLeft e | | Async<'T> | IO a | F# async → IO monad or async library | | Single-case union | newtype | Type safety wrappers |


Idiom Translation

Pattern: Discriminated Unions to Algebraic Data Types

F# and Haskell both have excellent sum type support, but syntax differs.

F#:

type PaymentMethod =
    | Cash
    | CreditCard of cardNumber: string
    | DebitCard of cardNumber: string * pin: int

let processPayment method =
    match method with
    | Cash -> "Processing cash"
    | CreditCard cardNum -> $"Credit card {cardNum}"
    | DebitCard (cardNum, _) -> $"Debit card {cardNum}"

// Using the type
let payment = CreditCard "1234-5678"
processPayment payment

Haskell:

data PaymentMethod
    = Cash
    | CreditCard { cardNumber :: String }
    | DebitCard { cardNumber :: String, pin :: Int }
    deriving (Show, Eq)

processPayment :: PaymentMethod -> String
processPayment Cash = "Processing cash"
processPayment (CreditCard cardNum) = "Credit card " ++ cardNum
processPayment (DebitCard cardNum _) = "Debit card " ++ cardNum

-- Using the type
payment :: PaymentMethod
payment = CreditCard "1234-5678"

result = processPayment payment

Why this translation:

  • Both languages have native sum types with similar semantics
  • F#'s of keyword → Haskell's constructor arguments
  • Named fields in F# → record syntax in Haskell (optional but idiomatic)
  • Pattern matching is nearly identical in both languages

Pattern: Records

Both languages have records, but F# updates are syntactically different.

F#:

type Person = {
    FirstName: string
    LastName: string
    Age: int
}

let person = {
    FirstName = "Alice"
    LastName = "Smith"
    Age = 30
}

// Copy-and-update
let olderPerson = { person with Age = 31 }

// Pattern matching
let getFullName { FirstName = f; LastName = l } = $"{f} {l}"

Haskell:

data Person = Person
    { firstName :: String
    , lastName :: String
    , age :: Int
    } deriving (Show, Eq)

person :: Person
person = Person
    { firstName = "Alice"
    , lastName = "Smith"
    , age = 30
    }

-- Record update syntax
olderPerson :: Person
olderPerson = person { age = 31 }

-- Pattern matching with record syntax
getFullName :: Person -> String
getFullName Person{ firstName = f, lastName = l } = f ++ " " ++ l

-- Or with field accessors
getFullName' :: Person -> String
getFullName' p = firstName p ++ " " ++ lastName p

Why this translation:

  • Both use similar record update syntax
  • Haskell's record syntax includes type constructor in pattern match
  • Haskell field accessors are automatic functions (firstName :: Person -> String)
  • Both are immutable by default

Pattern: Option/Result to Maybe/Either

F#'s Option and Result types map directly to Haskell's Maybe and Either.

F#:

// Option
let findUser id =
    if id = 1 then Some { Name = "Alice"; Age = 30 }
    else None

// Using Option
match findUser 1 with
| Some user -> user.Name
| None -> "Unknown"

// Option module functions
let result =
    findUser 1
    |> Option.map (fun u -> u.Name)
    |> Option.defaultValue "Unknown"

// Result
let divide x y =
    if y = 0 then Error "Division by zero"
    else Ok (x / y)

// Chaining Results
let calculate =
    result {
        let! a = divide 10 2
        let! b = divide 20 4
        return a + b
    }

Haskell:

-- Maybe
findUser :: Int -> Maybe Person
findUser 1 = Just Person { name = "Alice", age = 30 }
findUser _ = Nothing

-- Using Maybe
case findUser 1 of
    Just user -> name user
    Nothing -> "Unknown"

-- Maybe functions
result :: String
result = maybe "Unknown" name (findUser 1)

-- Or with fmap and fromMaybe
result' :: String
result' = fromMaybe "Unknown" (name <$> findUser 1)

-- Either
divide :: Double -> Double -> Either String Double
divide _ 0 = Left "Division by zero"
divide x y = Right (x / y)

-- Chaining Either (with do-notation)
calculate :: Either String Double
calculate = do
    a <- divide 10 2
    b <- divide 20 4
    return (a + b)

Why this translation:

  • Option → Maybe: Some becomes Just, None becomes Nothing
  • Result → Either: Ok becomes Right, Error becomes Left
  • F# computation expressions → Haskell do-notation
  • Both support similar functional combinators (map, bind, etc.)

Pattern: Computation Expressions to Do-Notation

F#'s computation expressions translate to Haskell's do-notation.

F#:

// Option workflow
type OptionBuilder() =
    member _.Bind(x, f) = Option.bind f x
    member _.Return(x) = Some x

let option = OptionBuilder()

let validateAge age =
    option {
        let! valid =
            if age >= 0 && age <= 120 then Some age
            else None
        return valid * 2
    }

// Async workflow
let fetchData url = async {
    printfn $"Fetching {url}..."
    do! Async.Sleep 1000
    return $"Data from {url}"
}

let processUrls urls = async {
    let! results =
        urls
        |> List.map fetchData
        |> Async.Parallel

    return results |> Array.toList
}

Haskell:

-- Maybe monad (built-in, no need for custom builder)
validateAge :: Int -> Maybe Int
validateAge age = do
    valid <- if age >= 0 && age <= 120
             then Just age
             else Nothing
    return (valid * 2)

-- IO monad for async operations
fetchData :: String -> IO String
fetchData url = do
    putStrLn $ "Fetching " ++ url ++ "..."
    threadDelay 1000000  -- 1 second in microseconds
    return $ "Data from " ++ url

-- Using Control.Concurrent.Async for parallel
import Control.Concurrent.Async (mapConcurrently)

processUrls :: [String] -> IO [String]
processUrls urls = mapConcurrently fetchData urls

Why this translation:

  • F# computation expressions ≈ Haskell monads
  • let! in F# ≈ <- in Haskell do-notation
  • return works the same in both
  • F# custom builders not needed in Haskell (monads are first-class)
  • F# Async.Parallel → Haskell mapConcurrently from async library

Pattern: Pipeline Operators

F# pipelines translate to Haskell application and composition.

F#:

// Forward pipe |>
let result =
    [1..10]
    |> List.filter (fun x -> x % 2 = 0)
    |> List.map (fun x -> x * x)
    |> List.sum

// Forward composition >>
let processData = List.filter even >> List.map ((*) 2) >> List.sum

result = processData [1..10]

// Backward pipe <|
let result = List.sum <| List.map ((*) 2) <| List.filter even [1..10]

Haskell:

-- Use $ for application (like <|)
result :: Int
result =
    sum $ map (^2) $ filter even [1..10]

-- Or use . for composition (backward, like <<)
processData :: [Int] -> Int
processData = sum . map (^2) . filter even

result' = processData [1..10]

-- For left-to-right, use & from Data.Function
import Data.Function ((&))

result'' :: Int
result'' =
    [1..10]
    & filter even
    & map (^2)
    & sum

Why this translation:

  • F# |> (forward) ↔ Haskell $ (application) or & (reverse application)
  • F# >> (forward composition) ↔ Haskell . (backward composition)
  • Haskell composition is right-to-left by default (like F#'s <<)
  • For F#-style left-to-right, use & operator

Pattern: Active Patterns to View Patterns / Pattern Synonyms

F# active patterns have no direct equivalent, but Haskell offers alternatives.

F#:

// Single-case active pattern
let (|Even|Odd|) n =
    if n % 2 = 0 then Even else Odd

match 42 with
| Even -> "even"
| Odd -> "odd"

// Partial active pattern
let (|Integer|_|) (str: string) =
    match System.Int32.TryParse(str) with
    | true, value -> Some value
    | false, _ -> None

match "123" with
| Integer n -> $"Number: {n}"
| _ -> "Not a number"

Haskell:

{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ViewPatterns #-}

-- View patterns approach
isEven :: Int -> Maybe ()
isEven n | even n = Just ()
         | otherwise = Nothing

isOdd :: Int -> Maybe ()
isOdd n | odd n = Just ()
        | otherwise = Nothing

-- Usage with view patterns
describe :: Int -> String
describe (isEven -> Just ()) = "even"
describe (isOdd -> Just ()) = "odd"

-- Pattern synonyms (simpler for this case)
pattern Even :: Int
pattern Even <- (even -> True)

pattern Odd :: Int
pattern Odd <- (odd -> True)

describe' :: Int -> String
describe' Even = "even"
describe' Odd = "odd"

-- Parsing integers
parseInteger :: String -> Maybe Int
parseInteger = readMaybe

describe'' :: String -> String
describe'' (parseInteger -> Just n) = "Number: " ++ show n
describe'' _ = "Not a number"

Why this translation:

  • F# active patterns → Haskell view patterns or pattern synonyms
  • View patterns more verbose but more powerful
  • Pattern synonyms simpler for basic cases
  • readMaybe from Text.Read provides safe parsing

Pattern: Type Providers to Runtime Parsing

F# type providers provide compile-time types from data. Haskell typically uses runtime parsing or Template Haskell.

F#:

open FSharp.Data

// CSV Type Provider (compile-time)
type StockData = CsvProvider<"stocks.csv">

let data = StockData.Load("stocks.csv")
for row in data.Rows do
    printfn $"{row.Date}: {row.Close}"

// JSON Type Provider
type Weather = JsonProvider<"""
{
    "temperature": 72,
    "condition": "sunny"
}
""">

let weather = Weather.Load("weather.json")
printfn $"Temperature: {weather.Temperature}°F"

Haskell:

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}

import Data.Aeson
import GHC.Generics
import qualified Data.ByteString.Lazy as B

-- Define types explicitly
data StockRow = StockRow
    { date :: String
    , close :: Double
    } deriving (Generic, Show)

instance FromJSON StockRow

-- Load and parse at runtime
loadStocks :: FilePath -> IO [StockRow]
loadStocks path = do
    contents <- B.readFile path
    case decode contents of
        Just rows -> return rows
        Nothing -> error "Failed to parse CSV"

-- JSON
data Weather = Weather
    { temperature :: Int
    , condition :: String
    } deriving (Generic, Show)

instance FromJSON Weather

loadWeather :: FilePath -> IO Weather
loadWeather path = do
    contents <- B.readFile path
    case decode contents of
        Just w -> return w
        Nothing -> error "Failed to parse JSON"

-- Alternative: Use Template Haskell for compile-time generation
{-# LANGUAGE TemplateHaskell #-}
import Data.Aeson.TH

$(deriveJSON defaultOptions ''Weather)  -- Generate instances

Why this translation:

  • F# type providers are compile-time, Haskell alternatives are runtime or TH
  • Haskell's aeson library provides safe JSON parsing with types
  • Template Haskell can generate instances but not infer from samples
  • Trade-off: Less magic, more explicit types, same type safety at use site

Error Handling

F# Result/Option → Haskell Either/Maybe

F# and Haskell have similar approaches to explicit error handling.

Comparison:

| Aspect | F# | Haskell | |--------|-----|---------| | Optional values | Option<'T> | Maybe a | | Result type | Result<'T,'E> | Either e a | | Success constructor | Ok, Some | Right, Just | | Failure constructor | Error, None | Left, Nothing | | Chaining | computation expressions | do-notation | | Standard library | Option, Result modules | Data.Maybe, Data.Either |

F#:

type ValidationError = string

let validateAge age =
    if age >= 0 && age <= 120 then
        Ok age
    else
        Error "Invalid age"

let validateEmail email =
    if email.Contains("@") then
        Ok email
    else
        Error "Invalid email"

// Chain validations
let validatePerson age email =
    result {
        let! validAge = validateAge age
        let! validEmail = validateEmail email
        return (validAge, validEmail)
    }

// Error: "Invalid age"
validatePerson (-1) "test@example.com"

// Ok: (30, "test@example.com")
validatePerson 30 "test@example.com"

Haskell:

type ValidationError = String

validateAge :: Int -> Either ValidationError Int
validateAge age
    | age >= 0 && age <= 120 = Right age
    | otherwise = Left "Invalid age"

validateEmail :: String -> Either ValidationError String
validateEmail email
    | '@' `elem` email = Right email
    | otherwise = Left "Invalid email"

-- Chain validations
validatePerson :: Int -> String -> Either ValidationError (Int, String)
validatePerson age email = do
    validAge <- validateAge age
    validEmail <- validateEmail email
    return (validAge, validEmail)

-- Left "Invalid age"
result1 = validatePerson (-1) "test@example.com"

-- Right (30, "test@example.com")
result2 = validatePerson 30 "test@example.com"

Why this translation:

  • F# Result<'T,'E> maps directly to Either e a
  • Note: Either puts error on left, value on right (opposite intuition)
  • Both support monadic composition (computation expressions / do-notation)
  • Both fail-fast: first error stops the chain

Concurrency Patterns

F# Async Workflows → Haskell IO and Async Library

F# async workflows are similar to Haskell's IO monad combined with the async library.

Comparison:

| Aspect | F# | Haskell | |--------|-----|---------| | Async type | Async<'T> | IO a (or Async a with library) | | Running | Async.RunSynchronously | No separate step (IO runs at main) | | Concurrency | Async.Parallel, Async.Start | forkIO, mapConcurrently, race | | Delay | Async.Sleep | threadDelay | | Bind syntax | let! / do! | <- in do-notation |

F#:

// Simple async
let fetchUser userId = async {
    printfn $"Fetching user {userId}"
    do! Async.Sleep 1000
    return { Id = userId; Name = "Alice" }
}

// Run async
let user = fetchUser 1 |> Async.RunSynchronously

// Parallel execution
let fetchMultiple userIds = async {
    let! users =
        userIds
        |> List.map fetchUser
        |> Async.Parallel

    return users |> Array.toList
}

// Fire-and-forget
Async.Start(fetchUser 1 |> Async.Ignore)

Haskell:

import Control.Concurrent (threadDelay, forkIO)
import Control.Concurrent.Async

-- IO action (like F# async)
fetchUser :: Int -> IO User
fetchUser userId = do
    putStrLn $ "Fetching user " ++ show userId
    threadDelay 1000000  -- 1 second (microseconds)
    return $ User userId "Alice"

-- Run IO (happens automatically in main)
main :: IO ()
main = do
    user <- fetchUser 1
    print user

-- Parallel execution with async library
fetchMultiple :: [Int] -> IO [User]
fetchMultiple userIds = mapConcurrently fetchUser userIds

-- Alternative: manual forking
fetchMultiple' :: [Int] -> IO [User]
fetchMultiple' userIds = do
    asyncs <- mapM (async . fetchUser) userIds
    mapM wait asyncs

-- Fire-and-forget
main' :: IO ()
main' = do
    forkIO $ fetchUser 1 >> return ()
    -- Continue with other work
    return ()

Why this translation:

  • F# Async<'T> conceptually similar to IO a
  • F# async needs explicit RunSynchronously; Haskell IO runs in main
  • Async.ParallelmapConcurrently from async library
  • threadDelay takes microseconds (1000000 = 1 second)
  • Haskell's async library provides structured concurrency

Memory & Ownership

F# (GC + Mutable) → Haskell (GC + Immutable)

Both languages are garbage-collected, but Haskell enforces immutability more strictly.

Comparison:

| Aspect | F# | Haskell | |--------|-----|---------| | Memory model | GC (via .NET CLR) | GC (via GHC runtime) | | Mutability | mutable keyword for fields | Immutable by default, IORef/STM for state | | References | ref cells | IORef, TVar, MVar | | Arrays | Mutable arrays available | Array (immutable), IOArray/STArray (mutable in IO/ST) | | Laziness | Eager by default | Lazy by default |

F#:

// Mutable field
type Counter() =
    let mutable count = 0
    member _.Increment() = count <- count + 1
    member _.Count = count

// Reference cell
let counter = ref 0
counter := !counter + 1
printfn $"Count: {!counter}"

// Mutable array (in-place updates)
let arr = [| 1; 2; 3 |]
arr.[0] <- 10

Haskell:

import Data.IORef
import Control.Monad (replicateM_)

-- Counter with IORef (mutable in IO)
data Counter = Counter { counterRef :: IORef Int }

newCounter :: IO Counter
newCounter = Counter <$> newIORef 0

increment :: Counter -> IO ()
increment (Counter ref) = modifyIORef' ref (+1)

getCount :: Counter -> IO Int
getCount (Counter ref) = readIORef ref

-- Usage
main :: IO ()
main = do
    counter <- newCounter
    replicateM_ 5 (increment counter)
    count <- getCount counter
    putStrLn $ "Count: " ++ show count

-- STRef for local mutation (pure interface)
import Control.Monad.ST
import Data.STRef

localMutation :: Int -> Int
localMutation n = runST $ do
    ref <- newSTRef 0
    replicateM_ n $ modifySTRef' ref (+1)
    readSTRef ref

Why this translation:

  • F# mutable / ref → Haskell IORef (requires IO monad)
  • For pure local mutation, use STRef with runST
  • Haskell's default immutability encourages functional patterns
  • Most code doesn't need mutable references

Common Pitfalls

1. Confusing Either Direction

Problem: F#'s Result has Ok/Error, Haskell's Either has Left/Right

-- WRONG: Thinking Right is error
validate x = if x > 0 then Left x else Right "Error"

-- CORRECT: Right is success, Left is error
validate x = if x > 0 then Right x else Left "Error"

Fix: Remember: "Right is right (correct)"

2. Missing IO for Side Effects

Problem: F# allows side effects anywhere, Haskell requires IO

-- WRONG: Can't use putStrLn in pure function
greet :: String -> String
greet name = putStrLn $ "Hello, " ++ name  -- Type error!

-- CORRECT: Return IO action
greet :: String -> IO ()
greet name = putStrLn $ "Hello, " ++ name

Fix: Mark functions with side effects as returning IO

3. Composition Direction

Problem: F# >> is forward, Haskell . is backward

-- F# style (forward)
-- let process = add1 >> double >> toString

-- WRONG direct translation
process = add1 . double . toString  -- Composes backward!

-- CORRECT: Reverse order
process = toString . double . add1

-- OR: Use <<< for forward composition (less idiomatic)
-- OR: Use & for application

Fix: Reverse composition order or use & for left-to-right

4. Lazy Evaluation Surprises

Problem: Haskell is lazy by default, F# is eager

-- F# would evaluate immediately
-- Haskell delays until needed
result = expensiveComputation 1000000

-- May cause space leaks
badSum = foldl (+) 0 [1..1000000]  -- Builds up thunks

-- CORRECT: Use strict operations when needed
goodSum = foldl' (+) 0 [1..1000000]  -- Strict fold

Fix: Use strict versions (foldl', seq, ! bang patterns) when needed

5. String Types Confusion

Problem: Haskell has multiple string types

-- String (lazy, inefficient)
name :: String
name = "Alice"

-- Text (strict, efficient)
name' :: Text
name' = "Alice"  -- Needs OverloadedStrings

-- ByteString (for binary data)
data' :: ByteString
data' = "binary"

Fix: Use Text for Unicode strings, ByteString for binary, String for simple cases

6. Type Class Constraints Not Explicit

Problem: F# inference works differently than Haskell's

-- F# infers numeric type
-- F# let add x y = x + y  (generic numeric)

-- Haskell needs constraint
add :: Num a => a -> a -> a
add x y = x + y

Fix: Add type signatures with constraints


Tooling

| Tool | F# | Haskell | Notes | |------|-----|---------|-------| | Build | dotnet build | cabal build / stack build | Stack is more batteries-included | | REPL | dotnet fsi | ghci / stack ghci | Interactive development | | Package Manager | NuGet (via dotnet) | Cabal / Hackage | Stack uses Stackage curated sets | | Formatter | Fantomas | ormolu / fourmolu / stylish-haskell | Multiple choices in Haskell | | Linter | FSharpLint | hlint | Suggests improvements | | Type Checker | F# compiler | GHC | GHC has more advanced type features | | Testing | Expecto, xUnit, FsUnit | HSpec, QuickCheck, Hedgehog, Tasty | Property testing popular in Haskell | | Language Server | Ionide | HLS (Haskell Language Server) | IDE integration |


Examples

Example 1: Simple - Record Type Conversion

Convert a simple F# record to Haskell.

Before (F#):

type User = {
    Id: int
    Name: string
    Email: string
    Age: int
}

let createUser id name email age = {
    Id = id
    Name = name
    Email = email
    Age = age
}

let getEmail user = user.Email

let updateAge age user = { user with Age = age }

// Usage
let user = createUser 1 "Alice" "alice@example.com" 30
let email = getEmail user
let older = updateAge 31 user

After (Haskell):

data User = User
    { userId :: Int
    , userName :: String
    , userEmail :: String
    , userAge :: Int
    } deriving (Show, Eq)

createUser :: Int -> String -> String -> Int -> User
createUser id' name email age = User
    { userId = id'
    , userName = name
    , userEmail = email
    , userAge = age
    }

getEmail :: User -> String
getEmail = userEmail  -- Record accessor is a function

updateAge :: Int -> User -> User
updateAge age user = user { userAge = age }

-- Usage
user :: User
user = createUser 1 "Alice" "alice@example.com" 30

email :: String
email = getEmail user

older :: User
older = updateAge 31 user

Example 2: Medium - Discriminated Unions and Pattern Matching

Convert F# discriminated unions and pattern matching.

Before (F#):

type Expression =
    | Literal of int
    | Add of Expression * Expression
    | Multiply of Expression * Expression
    | Variable of string

let rec evaluate env expr =
    match expr with
    | Literal n -> n
    | Add (left, right) ->
        evaluate env left + evaluate env right
    | Multiply (left, right) ->
        evaluate env left * evaluate env right
    | Variable name ->
        Map.find name env

let simplify expr =
    match expr with
    | Add (Literal 0, e) -> e
    | Add (e, Literal 0) -> e
    | Multiply (Literal 1, e) -> e
    | Multiply (e, Literal 1) -> e
    | Multiply (Literal 0, _) -> Literal 0
    | Multiply (_, Literal 0) -> Literal 0
    | e -> e

// Usage
let env = Map [("x", 5); ("y", 10)]
let expr = Add (Variable "x", Multiply (Literal 2, Variable "y"))
let result = evaluate env expr  // 5 + (2 * 10) = 25

After (Haskell):

import qualified Data.Map as Map

data Expression
    = Literal Int
    | Add Expression Expression
    | Multiply Expression Expression
    | Variable String
    deriving (Show, Eq)

type Environment = Map.Map String Int

evaluate :: Environment -> Expression -> Int
evaluate env (Literal n) = n
evaluate env (Add left right) =
    evaluate env left + evaluate env right
evaluate env (Multiply left right) =
    evaluate env left * evaluate env right
evaluate env (Variable name) =
    env Map.! name  -- Partial function, use Map.lookup for safety

simplify :: Expression -> Expression
simplify (Add (Literal 0) e) = e
simplify (Add e (Literal 0)) = e
simplify (Multiply (Literal 1) e) = e
simplify (Multiply e (Literal 1)) = e
simplify (Multiply (Literal 0) _) = Literal 0
simplify (Multiply _ (Literal 0)) = Literal 0
simplify e = e

-- Usage
env :: Environment
env = Map.fromList [("x", 5), ("y", 10)]

expr :: Expression
expr = Add (Variable "x") (Multiply (Literal 2) (Variable "y"))

result :: Int
result = evaluate env expr  -- 5 + (2 * 10) = 25

Example 3: Complex - Async Workflows to IO with Concurrency

Convert F# async workflows to Haskell IO with the async library.

Before (F#):

open System

type User = { Id: int; Name: string; Email: string }
type ApiError = NetworkError of string | ParseError of string

let fetchFromApi url = async {
    printfn $"Fetching from {url}"
    do! Async.Sleep 500
    if url.Contains("fail") then
        return Error (NetworkError "Network failed")
    else
        return Ok $"Data from {url}"
}

let parseUser data =
    if data.Contains("error") then
        Error (ParseError "Parse failed")
    else
        Ok { Id = 1; Name = "Alice"; Email = "alice@example.com" }

let getUserData url = async {
    let! fetchResult = fetchFromApi url
    return
        fetchResult
        |> Result.bind parseUser
}

let getAllUsers urls = async {
    let! results =
        urls
        |> List.map getUserData
        |> Async.Parallel

    return results |> Array.toList
}

// Usage
let urls = ["https://api.example.com/user/1"; "https://api.example.com/user/2"]
let results = getAllUsers urls |> Async.RunSynchronously

After (Haskell):

import Control.Concurrent (threadDelay)
import Control.Concurrent.Async (mapConcurrently)
import Data.List (isInfixOf)

data User = User
    { userId :: Int
    , userName :: String
    , userEmail :: String
    } deriving (Show, Eq)

data ApiError
    = NetworkError String
    | ParseError String
    deriving (Show, Eq)

fetchFromApi :: String -> IO (Either ApiError String)
fetchFromApi url = do
    putStrLn $ "Fetching from " ++ url
    threadDelay 500000  -- 500ms in microseconds
    return $ if "fail" `isInfixOf` url
             then Left (NetworkError "Network failed")
             else Right ("Data from " ++ url)

parseUser :: String -> Either ApiError User
parseUser dat
    | "error" `isInfixOf` dat = Left (ParseError "Parse failed")
    | otherwise = Right $ User 1 "Alice" "alice@example.com"

getUserData :: String -> IO (Either ApiError User)
getUserData url = do
    fetchResult <- fetchFromApi url
    return $ fetchResult >>= parseUser  -- Chaining Either with >>=

getAllUsers :: [String] -> IO [Either ApiError User]
getAllUsers urls = mapConcurrently getUserData urls

-- Usage
main :: IO ()
main = do
    let urls = ["https://api.example.com/user/1", "https://api.example.com/user/2"]
    results <- getAllUsers urls
    print results

See Also

For more examples and patterns, see:

  • meta-convert-dev - Foundational patterns with cross-language examples
  • lang-fsharp-dev - F# development patterns
  • lang-haskell-dev - Haskell development patterns

Cross-cutting pattern skills:

  • patterns-concurrency-dev - Async, STM, threading across languages
  • patterns-serialization-dev - JSON, validation across languages
  • patterns-metaprogramming-dev - Type providers vs Template Haskell