xiaohongshu
xiaohongshu
convert-fsharp-roc
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.
Full skill instructions
Convert F# code to idiomatic Roc. This skill extends meta-convert-dev with F#-to-Roc specific type mappings, idiom translations, and architectural guidance.
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.
meta-convert-devlang-fsharp-devlang-roc-devconvert-roc-fsharp| 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 ()) |
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 |
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.
| 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 () |
| 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 |
| 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# 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 |
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:
Option<'a> type; Roc uses tag unions [Some a, None]Option.map; Roc uses pattern matching with whenF#:
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:
result { ... }); Roc uses try operator (!)Error "msg" uses strings; Roc Err(DivByZero) uses typed tagsF#:
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:
filter → Roc keepIf (more descriptive name)sum → Roc walk(0, Num.add) (explicit fold)|>) idiomaticallyF#:
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:
match → Roc when (same exhaustiveness checking)$"{x}" → Roc interpolation \(x) (different syntax)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:
with keyword; Roc uses & operatorF#:
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:
>> (forward composition) and |> (pipeline)|> (pipeline) - prefer this styleF# 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:
Result typestry/with to Roc when ... is pattern matching! (try operator) for error propagation instead of exception bubblingF#:
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:
Result for success/failureThis 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:
Async<'a> → Roc Task a err (platform-provided)let! → Roc ! suffix (try operator)Async.Parallel → Roc Task.sequence (platform decides parallelism)Async.RunSynchronously; Roc main is already a TaskF#:
// 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:
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:
Task that the platform executesAssuming F# mutability works in Roc
mutable keyword and ref cellsTrying to use F# exceptions
raise, try/with, exception typesResult typeResult with typed errorsExpecting .NET BCL libraries
Using F# computation expressions freely
async { }, result { }, seq { }, etc.! operatorwhen ... is and ! for control flowAssuming type providers exist
Forgetting platform/application split
Using F# units of measure
[<Measure>] attribute for type-safe calculationsExpecting REPL-driven development
roc repl but it's more limitedexpect for inline tests insteadF#:
// 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:
exposes is explicit in Roc, implicit in F#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# (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:
expect statementsroc test| 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:
| 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 |
| 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 |
Before (F#):
type User = { Id: int; Name: string; Email: string }
let users = [
{ Id = 1; Name = "Alice"; Email = "[email protected]" }
{ Id = 2; Name = "Bob"; Email = "[email protected]" }
]
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: "[email protected]" },
{ id: 2, name: "Bob", email: "[email protected]" },
]
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"
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 })
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:
Async<'a> → Roc Task a err (platform-provided)try/with → Roc Result type with pattern matching! try operatorFor 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 patternsCross-cutting pattern skills:
patterns-concurrency-dev - Async workflows, Task model across languagespatterns-serialization-dev - JSON, validation across languagespatterns-metaprogramming-dev - Type providers vs code generationxiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience