| name | convert-fsharp-roc |
| description | Convert F# code to idiomatic Roc. Use when migrating F# projects to Roc, translating F# patterns to idiomatic Roc, or refactoring F# codebases. Extends meta-convert-dev with F#-to-Roc specific patterns. |
Convert F# to Roc
Convert F# code to idiomatic Roc. This skill extends meta-convert-dev with F#-to-Roc specific type mappings, idiom translations, and architectural guidance.
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# types → Roc types
- Idiom translations: F# patterns → idiomatic Roc
- Error handling: F# Result/Option → Roc Result/tag unions
- Platform shift: .NET runtime → Roc platform model
- Paradigm alignment: Both functional-first, but different architectures
This Skill Does NOT Cover
- General conversion methodology - see
meta-convert-dev - F# language fundamentals - see
lang-fsharp-dev - Roc language fundamentals - see
lang-roc-dev - Reverse conversion (Roc → F#) - see
convert-roc-fsharp
Quick Reference
| F# | Roc | Notes |
|---|---|---|
string |
Str |
Immutable strings |
int |
I64 |
Default signed integer |
float |
F64 |
64-bit floating point |
bool |
Bool |
Boolean values |
'a list |
List a |
Immutable lists |
'a array |
List a |
Arrays become lists |
Map<'k,'v> |
Dict k v |
Immutable dictionaries |
Set<'a> |
Set a |
Immutable sets |
Option<'a> |
[Some a, None] |
Optional values |
Result<'a,'e> |
Result a e |
Error handling |
| `{ | ... | }` anonymous record |
type X = ... discriminated union |
[...] tag union |
Sum types |
Async<'a> |
Task a err |
Async/effects via platform |
unit |
{} |
Empty record (not quite ()) |
When Converting Code
- Analyze source thoroughly before writing target
- Map types first - create type equivalence table
- Preserve semantics over syntax similarity
- Adopt Roc idioms - understand platform/application split
- Handle edge cases - null handling, error paths, effects
- Test equivalence - same inputs → same outputs
Paradigm Translation
Mental Model Shift: .NET Runtime → Platform Model
Both F# and Roc are functional-first languages, but they differ fundamentally in how they handle effects:
| F# Concept | Roc Approach | Key Insight |
|---|---|---|
| .NET runtime with GC | Platform provides runtime | Runtime is external to application |
Async<'a> workflows |
Task ok err via platform |
Effects delegated to platform |
| Direct I/O (Console, File) | Platform-provided I/O | Application remains pure |
| Mutable state allowed | Immutable by default | No mutable keyword |
| Exception handling | Result type only | No runtime exceptions |
| Type providers | Code generation external | No compile-time metaprogramming |
Architecture Mental Model
F# (.NET) Roc (Platform Model)
┌─────────────────────┐ ┌─────────────────────┐
│ Your F# Code │ │ Your Roc Code │
│ (can do I/O) │ │ (pure only) │
│ ↓ │ │ ↓ │
│ .NET BCL │ │ Platform API │
│ ↓ │ │ ↓ │
│ CLR Runtime │ │ Platform Host │
└─────────────────────┘ └─────────────────────┘
Everything in Clear separation
same runtime between pure & effects
Key shift: In F#, you can call Console.WriteLine anywhere. In Roc, all I/O goes through the platform's Task type.
Type System Mapping
Primitive Types
| F# | Roc | Notes |
|---|---|---|
string |
Str |
Both immutable UTF-8 |
int |
I64 |
F# int is 32-bit, Roc defaults to 64 |
int16, int32, int64 |
I16, I32, I64 |
Explicit sizes match |
uint16, uint32, uint64 |
U16, U32, U64 |
Unsigned variants |
byte |
U8 |
8-bit unsigned |
sbyte |
I8 |
8-bit signed |
float, double |
F32, F64 |
F# float is F64 |
decimal |
No direct equivalent | Use external library or F64 |
bool |
Bool |
Direct mapping |
char |
Use Str |
Roc has no char type |
unit |
{} |
Empty record, not () |
Collection Types
| F# | Roc | Notes |
|---|---|---|
'a list |
List a |
Both immutable, structural sharing |
'a array |
List a |
Roc lists handle array use cases |
'a seq |
List a |
Lazy sequences become lists |
Map<'k,'v> |
Dict k v |
Immutable maps |
Set<'a> |
Set a |
Immutable sets |
('a * 'b) tuple |
(a, b) |
Tuples map directly |
ResizeArray<'a> |
List a |
Mutable becomes immutable |
Composite Types
| F# | Roc | Notes |
|---|---|---|
type X = { ... } record |
{ ... } record |
Structural typing in both |
| `{ | ... | }` anonymous record |
type X = A | B | C DU |
[A, B, C] tag union |
Direct correspondence |
type X = A of int single-case DU |
[A I64] or opaque type |
For newtype, use opaque |
Option<'a> |
[Some a, None] |
Built-in DU vs tag union |
Result<'ok,'err> |
Result ok err |
Built-in DU vs tag union |
Choice<'a,'b> |
[A a, B b] tag union |
No built-in Choice |
F# Specific Types → Roc
| F# Type | Roc Strategy | Notes |
|---|---|---|
Async<'a> |
Task a err |
Platform-provided |
Task<'a> (.NET Task) |
Task a err |
Platform-provided |
Lazy<'a> |
Thunks ({} -> a) |
No built-in lazy |
ref<'a> |
Not needed | No mutable references |
'a -> 'b function |
a -> b |
Functions map directly |
| Type providers | External codegen | No compile-time metaprogramming |
| Units of measure | Custom validation | No built-in units |
Idiom Translation
Pattern 1: Option Handling
F#:
let findUser id =
users |> List.tryFind (fun u -> u.Id = id)
let userName =
findUser 1
|> Option.map (fun u -> u.Name)
|> Option.defaultValue "Unknown"
Roc:
findUser : I64 -> [Some User, None]
findUser = \id ->
List.findFirst(users, \u -> u.id == id)
|> Result.toOption # Convert Result to Option-like tag
userName =
when findUser(1) is
Some(u) -> u.name
None -> "Unknown"
Why this translation:
- F# has built-in
Option<'a>type; Roc uses tag unions[Some a, None] - F# has
Option.map; Roc uses pattern matching withwhen - Both are structural sum types under the hood
Pattern 2: Result for Error Handling
F#:
let divide x y =
if y = 0 then
Error "Division by zero"
else
Ok (x / y)
let calculate a b c =
result {
let! x = divide a b
let! y = divide x c
return y
}
Roc:
divide : I64, I64 -> Result I64 [DivByZero]
divide = \x, y ->
if y == 0 then
Err(DivByZero)
else
Ok(x // y)
calculate : I64, I64, I64 -> Result I64 [DivByZero]
calculate = \a, b, c ->
x = divide!(a, b) # Try operator for error propagation
y = divide!(x, c)
Ok(y)
Why this translation:
- F# has computation expressions (
result { ... }); Roc uses try operator (!) - F#
Error "msg"uses strings; RocErr(DivByZero)uses typed tags - Both propagate errors up the call stack
Pattern 3: List Operations
F#:
let result =
items
|> List.filter (fun x -> x.Active)
|> List.map (fun x -> x.Value)
|> List.sum
Roc:
result =
items
|> List.keepIf(\x -> x.active) # filter → keepIf
|> List.map(\x -> x.value)
|> List.walk(0, Num.add) # sum via walk (fold)
Why this translation:
- F#
filter→ RockeepIf(more descriptive name) - F#
sum→ Rocwalk(0, Num.add)(explicit fold) - Both use pipeline operator (
|>) idiomatically
Pattern 4: Pattern Matching
F#:
type Color =
| Red
| Green
| Blue
| Custom of r: int * g: int * b: int
let describe color =
match color with
| Red -> "red"
| Green -> "green"
| Blue -> "blue"
| Custom (r, g, b) -> $"rgb({r}, {g}, {b})"
Roc:
Color : [Red, Green, Blue, Custom(I64, I64, I64)]
describe : Color -> Str
describe = \color ->
when color is
Red -> "red"
Green -> "green"
Blue -> "blue"
Custom(r, g, b) -> "rgb(\(Num.toStr(r)), \(Num.toStr(g)), \(Num.toStr(b)))"
Why this translation:
- F# discriminated unions → Roc tag unions (nearly identical)
- F#
match→ Rocwhen(same exhaustiveness checking) - F# interpolation
$"{x}"→ Roc interpolation\(x)(different syntax)
Pattern 5: Record Updates
F#:
type Person = {
FirstName: string
LastName: string
Age: int
}
let person = { FirstName = "Alice"; LastName = "Smith"; Age = 30 }
let olderPerson = { person with Age = 31 }
Roc:
Person : {
firstName : Str,
lastName : Str,
age : U32,
}
person = { firstName: "Alice", lastName: "Smith", age: 30 }
olderPerson = { person & age: 31 }
Why this translation:
- F# uses
withkeyword; Roc uses&operator - Both create new records (copy-on-write)
- F# uses PascalCase by convention; Roc uses camelCase
Pattern 6: Pipeline Composition
F#:
let processUser =
fetchUser
>> validateUser
>> saveUser
// Or with pipe
let result =
userId
|> fetchUser
|> validateUser
|> saveUser
Roc:
# Roc doesn't have >> composition operator
# Use pipeline instead
result =
userId
|> fetchUser
|> validateUser
|> saveUser
Why this translation:
- F# has both
>>(forward composition) and|>(pipeline) - Roc only has
|>(pipeline) - prefer this style - Same left-to-right data flow
Error Handling
F# Exception Model → Roc Result Model
F# supports both exceptions and Result<'a,'e>. Roc only has Result.
F#:
// Style 1: Exceptions
let divide x y =
if y = 0 then
raise (DivideByZeroException())
else
x / y
try
let result = divide 10 0
printfn $"Result: {result}"
with
| :? DivideByZeroException -> printfn "Cannot divide by zero"
// Style 2: Result (preferred for F# interop)
let safeDivide x y =
if y = 0 then
Error "Division by zero"
else
Ok (x / y)
Roc:
# Only Result style - no exceptions
divide : I64, I64 -> Result I64 [DivByZero]
divide = \x, y ->
if y == 0 then
Err(DivByZero)
else
Ok(x // y)
# Handling
when divide(10, 0) is
Ok(result) -> Stdout.line!("Result: \(Num.toStr(result))")
Err(DivByZero) -> Stdout.line!("Cannot divide by zero")
Migration strategy:
- Convert all F# exceptions to Roc
Resulttypes - Convert F#
try/withto Rocwhen ... ispattern matching - Use
!(try operator) for error propagation instead of exception bubbling
Multiple Error Types
F#:
type ValidationError =
| EmptyName
| InvalidAge
| InvalidEmail
let validatePerson name age email =
if String.IsNullOrWhiteSpace(name) then
Error EmptyName
elif age < 0 || age > 120 then
Error InvalidAge
elif not (email.Contains("@")) then
Error InvalidEmail
else
Ok { Name = name; Age = age; Email = email }
Roc:
ValidationError : [EmptyName, InvalidAge, InvalidEmail]
validatePerson : Str, I64, Str -> Result Person ValidationError
validatePerson = \name, age, email ->
if Str.isEmpty(name) then
Err(EmptyName)
else if age < 0 || age > 120 then
Err(InvalidAge)
else if !(Str.contains(email, "@")) then
Err(InvalidEmail)
else
Ok({ name, age, email })
Why this translation:
- Both use discriminated unions/tag unions for error types
- Both use
Resultfor success/failure - Both have exhaustive pattern matching
Async and Effects
F# Async → Roc Task
This is a significant paradigm shift. F# Async runs on the .NET runtime; Roc Task is platform-provided.
F#:
let fetchData url = async {
let! response = httpClient.GetStringAsync(url) |> Async.AwaitTask
return response
}
let processMultiple urls = async {
let! results =
urls
|> List.map fetchData
|> Async.Parallel
return Array.toList results
}
// Run the async
let result = processMultiple urls |> Async.RunSynchronously
Roc:
# Platform provides Task and Http
import pf.Http
import pf.Task exposing [Task]
fetchData : Str -> Task Str [HttpErr]
fetchData = \url ->
Http.get!(url) # Platform handles async
processMultiple : List Str -> Task (List Str) [HttpErr]
processMultiple = \urls ->
# Platform may parallelize this
urls
|> List.map(fetchData)
|> Task.sequence # Platform-provided
# main is already a Task - no explicit run
main : Task {} []
main =
results = processMultiple!(urls)
Stdout.line!("Done")
Why this translation:
- F#
Async<'a>→ RocTask a err(platform-provided) - F#
let!→ Roc!suffix (try operator) - F#
Async.Parallel→ RocTask.sequence(platform decides parallelism) - F# needs
Async.RunSynchronously; Rocmainis already a Task
Pure vs Effectful Code
F#:
// Pure computation
let add x y = x + y
// Effectful computation (can do I/O anywhere)
let greet name =
printfn $"Hello, {name}!"
name
Roc:
# Pure computation
add : I64, I64 -> I64
add = \x, y -> x + y
# Effectful computation (must return Task)
greet : Str -> Task Str []
greet = \name ->
Stdout.line!("Hello, \(name)!")
Task.ok(name) # Return pure value in Task
Migration strategy:
- Identify all F# code that does I/O
- Restructure to separate pure logic from effects
- Move effects to platform Task boundaries
- Keep business logic pure
Platform Architecture
.NET Application → Roc Application + Platform
F# (.NET Console App):
[<EntryPoint>]
let main argv =
let input = Console.ReadLine()
let processed = processInput input
Console.WriteLine(processed)
0 // Return exit code
Roc (Platform-based):
app [main] {
pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br"
}
import pf.Stdin
import pf.Stdout
import pf.Task exposing [Task]
main : Task {} []
main =
input = Stdin.line!
processed = processInput(input) # Pure function
Stdout.line!(processed)
# Pure helper (no effects)
processInput : Str -> Str
processInput = \input ->
Str.toUpper(input)
Key differences:
- F# entry point is a function that returns int (exit code)
- Roc entry point is a
Taskthat the platform executes - F# can call I/O anywhere; Roc separates pure from effectful code
Common Pitfalls
Assuming F# mutability works in Roc
- F# allows
mutablekeyword andrefcells - Roc has no mutable variables
- Fix: Redesign with immutable data structures
- F# allows
Trying to use F# exceptions
- F# has
raise,try/with, exception types - Roc only has
Resulttype - Fix: Convert all exceptions to
Resultwith typed errors
- F# has
Expecting .NET BCL libraries
- F# has access to entire .NET Base Class Library
- Roc only has what the platform provides
- Fix: Check platform docs for available APIs
Using F# computation expressions freely
- F# has
async { },result { },seq { }, etc. - Roc only has pattern matching and
!operator - Fix: Use
when ... isand!for control flow
- F# has
Assuming type providers exist
- F# type providers generate types at compile time
- Roc has no metaprogramming
- Fix: Use external code generation tools
Forgetting platform/application split
- F# code is all in the same runtime
- Roc strictly separates pure (app) from effects (platform)
- Fix: Keep business logic pure, push effects to boundaries
Using F# units of measure
- F# has
[<Measure>]attribute for type-safe calculations - Roc has no built-in units
- Fix: Use opaque types with smart constructors for validation
- F# has
Expecting REPL-driven development
- F# has F# Interactive (FSI) for REPL workflows
- Roc supports
roc replbut it's more limited - Fix: Use
expectfor inline tests instead
Module System
F# Modules/Namespaces → Roc Interfaces
F#:
// UserModule.fs
namespace MyApp
module User =
type User = {
Id: int
Name: string
Email: string
}
let create name email = {
Id = generateId()
Name = name
Email = email
}
let getName user = user.Name
Roc:
# User.roc
interface User
exposes [User, create, getName]
imports []
User : {
id : I64,
name : Str,
email : Str,
}
create : Str, Str -> User
create = \name, email -> {
id: generateId(),
name,
email,
}
getName : User -> Str
getName = \user -> user.name
Migration notes:
- F# namespaces → Not needed in Roc (file-based modules)
- F# modules → Roc interfaces
- F#
exposesis explicit in Roc, implicit in F#
Build System
.NET Project → Roc Application
F# (.fsproj):
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="Types.fs" />
<Compile Include="Logic.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FSharp.Data" Version="6.3.0" />
</ItemGroup>
</Project>
Roc:
# main.roc - single file or multiple interfaces
app [main] {
pf: platform "https://..."
}
import Types
import Logic
main : Task {} []
main =
Logic.run
Build commands:
# F#
dotnet build
dotnet run
# Roc
roc build main.roc
roc run main.roc
Key differences:
- F# needs .fsproj and explicit file ordering
- Roc infers dependencies from imports
- F# uses NuGet for packages; Roc uses platform URLs
Testing
F# Testing → Roc Expect
F# (Expecto):
module Tests
open Expecto
[<Tests>]
let tests =
testList "Math tests" [
testCase "addition" <| fun () ->
Expect.equal (2 + 2) 4 "2 + 2 = 4"
testCase "division by zero" <| fun () ->
let result = divide 10 0
Expect.equal result (Error "Division by zero") "should error"
]
[<EntryPoint>]
let main args =
runTestsWithCLIArgs [] args tests
Roc:
# Inline tests with expect
add : I64, I64 -> I64
add = \x, y -> x + y
expect add(2, 2) == 4
divide : I64, I64 -> Result I64 [DivByZero]
divide = \x, y ->
if y == 0 then
Err(DivByZero)
else
Ok(x // y)
expect divide(10, 0) == Err(DivByZero)
expect divide(10, 2) == Ok(5)
Run tests:
# F#
dotnet test
# Roc
roc test main.roc
Migration strategy:
- Convert Expecto/xUnit/NUnit tests to Roc
expectstatements - Place expects near the functions they test
- Run with
roc test
Limitations
Coverage Gaps
| Pillar | F# Skill | Roc Skill | Mitigation |
|---|---|---|---|
| Module | ✓ | ✓ | Both well-documented |
| Error | ✓ (Result + exceptions) | ✓ (Result only) | See Error Handling section |
| Concurrency | ~ (Async covered) | ✓ | See Async and Effects section |
| Metaprogramming | ~ (Type providers) | ✓ (minimalist) | External code generation |
| Zero/Default | ✓ (implicit) | ~ (via pattern matching) | Use tag unions for nullable |
| Serialization | ✓ | ~ (via abilities) | See patterns-serialization-dev |
| Build | ✓ | ~ (emerging) | Roc build system is simpler |
| Testing | ✓ | ✓ | Both covered adequately |
Combined Score: 14/16 (Good)
Known Limitations:
- Metaprogramming: F# type providers have no Roc equivalent; use external codegen
- Serialization: F# has rich JSON/XML libraries; Roc relies on platform Encode/Decode abilities
- Concurrency: F# Async is mature; Roc Task model is platform-dependent
External Resources Used
| Resource | What It Provided | Reliability |
|---|---|---|
| F# for Fun and Profit | Idiom examples | High |
| Roc Tutorial | Platform model guidance | High |
| lang-fsharp-dev | Type system details | High |
| lang-roc-dev | Task and platform patterns | High |
Tooling
| Tool | Purpose | Notes |
|---|---|---|
roc CLI |
Build, run, test, format | Equivalent to dotnet CLI |
| Roc LSP | Editor support | VS Code, vim, etc. |
roc format |
Code formatting | Like fantomas for F# |
roc test |
Run inline expects | Like dotnet test |
| External codegen | Type generation | Replaces F# type providers |
Examples
Example 1: Simple - Option Handling
Before (F#):
type User = { Id: int; Name: string; Email: string }
let users = [
{ Id = 1; Name = "Alice"; Email = "alice@example.com" }
{ Id = 2; Name = "Bob"; Email = "bob@example.com" }
]
let findUserById id =
users |> List.tryFind (fun u -> u.Id = id)
let getUserName id =
findUserById id
|> Option.map (fun u -> u.Name)
|> Option.defaultValue "Unknown"
After (Roc):
User : { id : I64, name : Str, email : Str }
users = [
{ id: 1, name: "Alice", email: "alice@example.com" },
{ id: 2, name: "Bob", email: "bob@example.com" },
]
findUserById : I64 -> [Some User, None]
findUserById = \id ->
when List.findFirst(users, \u -> u.id == id) is
Ok(user) -> Some(user)
Err(_) -> None
getUserName : I64 -> Str
getUserName = \id ->
when findUserById(id) is
Some(u) -> u.name
None -> "Unknown"
Example 2: Medium - Result Error Handling
Before (F#):
type ValidationError =
| InvalidName
| InvalidAge
type Person = { Name: string; Age: int }
let validateName name =
if String.IsNullOrWhiteSpace(name) then
Error InvalidName
else
Ok name
let validateAge age =
if age < 0 || age > 120 then
Error InvalidAge
else
Ok age
let createPerson name age =
result {
let! validName = validateName name
let! validAge = validateAge age
return { Name = validName; Age = validAge }
}
After (Roc):
ValidationError : [InvalidName, InvalidAge]
Person : { name : Str, age : I64 }
validateName : Str -> Result Str [InvalidName]
validateName = \name ->
if Str.isEmpty(name) then
Err(InvalidName)
else
Ok(name)
validateAge : I64 -> Result I64 [InvalidAge]
validateAge = \age ->
if age < 0 || age > 120 then
Err(InvalidAge)
else
Ok(age)
createPerson : Str, I64 -> Result Person [InvalidName, InvalidAge]
createPerson = \name, age ->
validName = validateName!(name)
validAge = validateAge!(age)
Ok({ name: validName, age: validAge })
Example 3: Complex - Async File Processing
Before (F#):
open System.IO
type ProcessingError =
| FileNotFound of string
| InvalidFormat of string
let readFile path = async {
try
let! content = File.ReadAllTextAsync(path) |> Async.AwaitTask
return Ok content
with
| :? FileNotFoundException ->
return Error (FileNotFound path)
}
let processContent content =
if content.Contains("error") then
Error (InvalidFormat "Content contains error")
else
Ok (content.ToUpper())
let writeFile path content = async {
do! File.WriteAllTextAsync(path, content) |> Async.AwaitTask
return Ok ()
}
let processFile inputPath outputPath = async {
let! contentResult = readFile inputPath
match contentResult with
| Error e -> return Error e
| Ok content ->
match processContent content with
| Error e -> return Error e
| Ok processed ->
return! writeFile outputPath processed
}
// Usage
let result =
processFile "input.txt" "output.txt"
|> Async.RunSynchronously
After (Roc):
app [main] {
pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br"
}
import pf.File
import pf.Path
import pf.Task exposing [Task]
import pf.Stdout
ProcessingError : [FileNotFound Str, InvalidFormat Str]
readFile : Str -> Task Str [FileReadErr Path.ReadErr]*
readFile = \path ->
File.readUtf8(Path.fromStr(path))
processContent : Str -> Result Str [InvalidFormat Str]
processContent = \content ->
if Str.contains(content, "error") then
Err(InvalidFormat("Content contains error"))
else
Ok(Str.toUpper(content))
writeFile : Str, Str -> Task {} [FileWriteErr Path.WriteErr]*
writeFile = \path, content ->
File.writeUtf8(Path.fromStr(path), content)
processFile : Str, Str -> Task {} [FileReadErr Path.ReadErr, InvalidFormat Str, FileWriteErr Path.WriteErr]*
processFile = \inputPath, outputPath ->
# Read file (returns Task)
content = readFile!(inputPath)
# Process content (pure function, returns Result)
processed = processContent!(content)
# Write file (returns Task)
writeFile!(outputPath, processed)
main : Task {} []
main =
when processFile("input.txt", "output.txt") is
Ok({}) -> Stdout.line!("File processed successfully")
Err(FileReadErr(_)) -> Stdout.line!("Error reading file")
Err(InvalidFormat(msg)) -> Stdout.line!("Invalid format: \(msg)")
Err(FileWriteErr(_)) -> Stdout.line!("Error writing file")
Key conversions:
- F#
Async<'a>→ RocTask a err(platform-provided) - F#
try/with→ Roc Result type with pattern matching - F# computation expression → Roc
!try operator - F# can mix pure/async; Roc separates Task boundaries
See Also
For more examples and patterns, see:
meta-convert-dev- Foundational patterns with cross-language examplesconvert-elm-clojure- Another functional language pair conversion (similar paradigm shifts)lang-fsharp-dev- F# development patternslang-roc-dev- Roc development patterns
Cross-cutting pattern skills:
patterns-concurrency-dev- Async workflows, Task model across languagespatterns-serialization-dev- JSON, validation across languagespatterns-metaprogramming-dev- Type providers vs code generation