xiaohongshu
xiaohongshu
convert-roc-fsharp
Convert Roc code to idiomatic F#. Use when migrating Roc projects to F#, translating Roc patterns to idiomatic F#, or refactoring Roc codebases. Extends meta-convert-dev with Roc-to-F# specific patterns.
Full skill instructions
Convert Roc code to idiomatic F#. This skill extends meta-convert-dev with Roc-to-F# 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-roc-devlang-fsharp-devconvert-fsharp-roc| Roc | F# | Notes |
|---|---|---|
Str | string | Immutable strings |
I64 | int64 | 64-bit signed integer |
I32 | int | F# default int is 32-bit |
F64 | float or double | 64-bit floating point |
Bool | bool | Boolean values |
List a | 'a list | Immutable lists |
Dict k v | Map<'k,'v> | Immutable dictionaries |
Set a | Set<'a> | Immutable sets |
[Some a, None] | Option<'a> | Optional values |
Result a e | Result<'a,'e> | Error handling |
{ ... } record | `{ | ... |
[...] tag union | type X = ... discriminated union | Sum types |
Task a err | Async<'a> or Task<'a> | Async/effects |
{} | unit | Empty value |
Both Roc and F# are functional-first languages, but they differ fundamentally in how they handle effects:
| Roc Concept | F# Approach | Key Insight |
|---|---|---|
| Platform provides runtime | .NET CLR runtime | Runtime is part of the application |
Task ok err via platform | Async<'a> workflows | Effects integrated into language |
| Platform-provided I/O | Direct I/O (Console, File) | Can do I/O anywhere |
| Application remains pure | Can mix pure and impure | Flexibility over purity |
| No runtime exceptions | Exception handling | Exceptions are first-class |
| No compile-time metaprogramming | Type providers | Rich compile-time features |
Roc (Platform Model) F# (.NET)
┌─────────────────────┐ ┌─────────────────────┐
│ Your Roc Code │ │ Your F# Code │
│ (pure only) │ │ (can do I/O) │
│ ↓ │ │ ↓ │
│ Platform API │ │ .NET BCL │
│ ↓ │ │ ↓ │
│ Platform Host │ │ CLR Runtime │
└─────────────────────┘ └─────────────────────┘
Clear separation Everything in
between pure & effects same runtime
Key shift: In Roc, I/O goes through the platform's Task type. In F#, you can call Console.WriteLine or perform I/O anywhere.
| Roc | F# | Notes |
|---|---|---|
Str | string | Both immutable UTF-8 |
I8 | sbyte | 8-bit signed |
I16 | int16 | 16-bit signed |
I32 | int | F# default int is 32-bit |
I64 | int64 or long | 64-bit signed |
I128 | System.Numerics.BigInteger | No native 128-bit int |
U8 | byte | 8-bit unsigned |
U16 | uint16 | 16-bit unsigned |
U32 | uint32 | 32-bit unsigned |
U64 | uint64 or ulong | 64-bit unsigned |
U128 | System.Numerics.BigInteger | No native 128-bit uint |
F32 | float32 or single | 32-bit floating point |
F64 | float or double | 64-bit floating point (F# default) |
Bool | bool | Direct mapping |
{} | unit | Empty value |
| Roc | F# | Notes |
|---|---|---|
List a | 'a list | Both immutable, structural sharing |
Dict k v | Map<'k,'v> | Immutable dictionaries |
Set a | Set<'a> | Immutable sets |
(a, b) | 'a * 'b | Tuples map directly |
(a, b, c) | 'a * 'b * 'c | Multiple element tuples |
| Roc | F# | Notes |
|---|---|---|
{ ... } record | `{ | ... |
{ ... } record | type X = { ... } record | Nominal typing (preferred) |
[A, B, C] tag union | type X = A | B | C DU | Direct correspondence |
[A I64, B Str] tag with payload | type X = A of int64 | B of string | Payload mapping |
[Some a, None] | Option<'a> | Built-in option type |
Result ok err | Result<'ok,'err> | Built-in result type |
| Roc Type | F# Strategy | Notes |
|---|---|---|
Task a err | Async<'a> or Task<'a> | Platform effects → runtime async |
| Opaque types | Single-case DU | type Email = Email of string |
| Tag unions (open) | Extensible DU (rare) | Use closed DU instead |
| Abilities constraints | Interface constraints | 'a when 'a :> IEquatable<'a> |
Roc:
Color : [Red, Green, Blue, Custom(U8, U8, U8)]
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)))"
F#:
type Color =
| Red
| Green
| Blue
| Custom of r: byte * g: byte * b: byte
let describe color =
match color with
| Red -> "red"
| Green -> "green"
| Blue -> "blue"
| Custom (r, g, b) -> $"rgb({r}, {g}, {b})"
Why this translation:
when → F# match (same exhaustiveness checking)\(x) → F# interpolation $"{x}" or {x}Roc:
findUser : I64 -> [Some User, None]
findUser = \id ->
List.findFirst(users, \u -> u.id == id)
|> Result.toOption
userName =
when findUser(1) is
Some(u) -> u.name
None -> "Unknown"
F#:
let findUser id =
users |> List.tryFind (fun u -> u.Id = id)
let userName =
match findUser 1 with
| Some u -> u.Name
| None -> "Unknown"
Why this translation:
[Some a, None] → F# Option<'a> (built-in type)Option.map, Option.bind helpers not shown in Roc exampleRoc:
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)
y = divide!(x, c)
Ok(y)
F#:
type DivisionError = DivByZero
let divide x y =
if y = 0 then
Error DivByZero
else
Ok (x / y)
let calculate a b c =
result {
let! x = divide a b
let! y = divide x c
return y
}
Why this translation:
Result ok err → F# Result<'ok,'err> (built-in)! try operator → F# let! in computation expression[DivByZero] → F# DU type DivisionError = DivByZeroRoc:
result =
items
|> List.keepIf(\x -> x.active)
|> List.map(\x -> x.value)
|> List.walk(0, Num.add)
F#:
let result =
items
|> List.filter (fun x -> x.Active)
|> List.map (fun x -> x.Value)
|> List.sum
Why this translation:
keepIf → F# filter (different naming)walk(0, Num.add) → F# sum (built-in helper)sum, average, etc.)Roc:
Person : {
firstName : Str,
lastName : Str,
age : U32,
}
person = { firstName: "Alice", lastName: "Smith", age: 30 }
olderPerson = { person & age: 31 }
F#:
type Person = {
FirstName: string
LastName: string
Age: int
}
let person = { FirstName = "Alice"; LastName = "Smith"; Age = 30 }
let olderPerson = { person with Age = 31 }
Why this translation:
& operator → F# with keyword (copy-and-update); for field separators, Roc uses commasRoc:
result =
userId
|> fetchUser
|> validateUser
|> saveUser
F#:
// Same pipeline style
let result =
userId
|> fetchUser
|> validateUser
|> saveUser
// Or with composition
let processUser =
fetchUser
>> validateUser
>> saveUser
let result = processUser userId
Why this translation:
|> for pipeline (identical)>> composition operator (Roc doesn't)Roc only has Result. F# supports both Result<'a,'e> and exceptions.
Roc:
divide : I64, I64 -> Result I64 [DivByZero]
divide = \x, y ->
if y == 0 then
Err(DivByZero)
else
Ok(x // y)
when divide(10, 0) is
Ok(result) -> Stdout.line!("Result: \(Num.toStr(result))")
Err(DivByZero) -> Stdout.line!("Cannot divide by zero")
F# (Result style - preferred):
type DivisionError = DivByZero
let divide x y =
if y = 0 then
Error DivByZero
else
Ok (x / y)
match divide 10 0 with
| Ok result -> printfn $"Result: {result}"
| Error DivByZero -> printfn "Cannot divide by zero"
F# (Exception style - for interop):
let divide x y =
if y = 0 then
raise (System.DivideByZeroException())
else
x / y
try
let result = divide 10 0
printfn $"Result: {result}"
with
| :? System.DivideByZeroException -> printfn "Cannot divide by zero"
Migration strategy:
Result type for functional code (matches Roc semantics)when ... is to F# match ... withRoc:
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 })
F#:
type ValidationError =
| EmptyName
| InvalidAge
| InvalidEmail
type Person = {
Name: string
Age: int
Email: string
}
let validatePerson name age email =
if System.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 }
Why this translation:
if/else if → F# if/elif (same control flow)Result with typed errorsThis is a significant paradigm shift. Roc Task is platform-provided; F# Async is built into the language.
Roc:
import pf.Http
import pf.Task exposing [Task]
fetchData : Str -> Task Str [HttpErr]
fetchData = \url ->
Http.get!(url)
processMultiple : List Str -> Task (List Str) [HttpErr]
processMultiple = \urls ->
urls
|> List.map(fetchData)
|> Task.sequence
main : Task {} []
main =
results = processMultiple!(urls)
Stdout.line!("Done")
F#:
open System.Net.Http
type HttpError = HttpErr of string
let httpClient = new HttpClient()
let fetchData url = async {
try
let! response = httpClient.GetStringAsync(url) |> Async.AwaitTask
return Ok response
with
| ex -> return Error (HttpErr ex.Message)
}
let processMultiple urls = async {
let! results =
urls
|> List.map fetchData
|> Async.Parallel
return Array.toList results |> List.choose id // Extract Ok values
}
[<EntryPoint>]
let main argv =
let urls = ["url1"; "url2"; "url3"]
processMultiple urls
|> Async.RunSynchronously
|> ignore
printfn "Done"
0
Why this translation:
Task a err → F# Async<Result<'a, 'err>> (effects + errors)! operator → F# let! in async { } blockAsync.RunSynchronouslymain is a Task → F# main returns int (exit code)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)
F#:
// Pure computation
let add x y = x + y
// Effectful computation (no special type required)
let greet name =
printfn $"Hello, {name}!"
name // Can return pure value directly
// Or as Async if needed
let greetAsync name = async {
printfn $"Hello, {name}!"
return name
}
Migration strategy:
Task functions → F# Async or direct I/O (depends on context)! try operator → F# let! or do!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)
Stdout.line!(processed)
processInput : Str -> Str
processInput = \input ->
Str.toUpper(input)
F# (.NET Console App):
[<EntryPoint>]
let main argv =
let input = System.Console.ReadLine()
let processed = processInput input
System.Console.WriteLine(processed)
0 // Return exit code
let processInput input =
input.ToUpper()
Key differences:
Task that platform executesForgetting F# allows mutability
mutable keyword and ref cellsNot leveraging F# exceptions
Result typeResult and exceptionsResult for domain errors, exceptions for unexpected failuresMissing .NET BCL libraries
Not using F# computation expressions
! operatorasync { }, result { }, seq { }, etc.Ignoring F# type providers
Assuming strict platform/application split
Not using F# units of measure
[<Measure>] for type-safe calculationsMissing F# Interactive (REPL)
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
F#:
// User.fs
namespace MyApp
module User =
type User = {
Id: int64
Name: string
Email: string
}
let create name email = {
Id = generateId()
Name = name
Email = email
}
let getName user = user.Name
Migration notes:
exposes is explicit → F# exports everything by defaultRoc:
# main.roc - single file or multiple interfaces
app [main] {
pf: platform "https://..."
}
import Types
import Logic
main : Task {} []
main =
Logic.run
Build commands:
roc build main.roc
roc run main.roc
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>
Build commands:
dotnet build
dotnet run
Key differences:
Roc:
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:
roc test main.roc
F# (Expecto):
module Tests
open Expecto
let add x y = x + y
[<Tests>]
let tests =
testList "Math tests" [
testCase "addition" <| fun () ->
Expect.equal (add 2 2) 4 "2 + 2 = 4"
testCase "division by zero" <| fun () ->
let result = divide 10L 0L
Expect.equal result (Error DivByZero) "should error"
testCase "division success" <| fun () ->
let result = divide 10L 2L
Expect.equal result (Ok 5L) "10 / 2 = 5"
]
[<EntryPoint>]
let main args =
runTestsWithCLIArgs [] args tests
Run tests:
dotnet test
Migration strategy:
expect statements to test framework assertionsBefore (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"
After (F#):
type User = {
Id: int64
Name: string
Email: string
}
let users = [
{ Id = 1L; Name = "Alice"; Email = "[email protected]" }
{ Id = 2L; Name = "Bob"; Email = "[email protected]" }
]
let findUserById id =
users |> List.tryFind (fun u -> u.Id = id)
let getUserName id =
match findUserById id with
| Some u -> u.Name
| None -> "Unknown"
Before (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 })
After (F#):
type ValidationError =
| InvalidName
| InvalidAge
type Person = {
Name: string
Age: int
}
let validateName name =
if System.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 }
}
Before (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 ->
content = readFile!(inputPath)
processed = processContent!(content)
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")
After (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)
| ex ->
return Error (FileNotFound $"Error: {ex.Message}")
}
let processContent content =
if content.Contains("error") then
Error (InvalidFormat "Content contains error")
else
Ok (content.ToUpper())
let writeFile path content = async {
try
do! File.WriteAllTextAsync(path, content) |> Async.AwaitTask
return Ok ()
with
| ex ->
return Error (FileNotFound $"Write error: {ex.Message}")
}
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
}
[<EntryPoint>]
let main argv =
let result =
processFile "input.txt" "output.txt"
|> Async.RunSynchronously
match result with
| Ok () -> printfn "File processed successfully"
| Error (FileNotFound msg) -> printfn $"File error: {msg}"
| Error (InvalidFormat msg) -> printfn $"Invalid format: {msg}"
0
Key conversions:
Task a err → F# Async<Result<'a, 'err>>! operator → F# let! in async blocksmain returns Task → F# main returns intFor more examples and patterns, see:
meta-convert-dev - Foundational patterns with cross-language examplesconvert-fsharp-roc - Reverse conversion (F# → Roc)lang-roc-dev - Roc development patternslang-fsharp-dev - F# development patternsCross-cutting pattern skills:
patterns-concurrency-dev - Task model vs Async workflowspatterns-serialization-dev - JSON, validation across languagespatterns-metaprogramming-dev - No metaprogramming vs type providersxiaohongshu
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