Bidirectional conversion between Fsharp and Roc. Use when migrating projects between these languages in either direction. Extends meta-convert-dev with Fsharp↔Roc specific patterns.
Scanned 9/6/2026
Install to Claude Code
npx -y skills add aRustyDev/agents --skill convert-fsharp-roc --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Convert Fsharp Roc?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/arustydev-convert-fsharp-roc-agents)More formats (shields.io, HTML) on the badges page.
---
name: convert-fsharp-roc
description: Bidirectional conversion between Fsharp and Roc. Use when migrating projects between these languages in either direction. Extends meta-convert-dev with Fsharp↔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`
---
## 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 | `{ ... }` record | Structural records |
| `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
1. **Analyze source thoroughly** before writing target
2. **Map types first** - create type equivalence table
3. **Preserve semantics** over syntax similarity
4. **Adopt Roc idioms** - understand platform/application split
5. **Handle edge cases** - null handling, error paths, effects
6. **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 | `{ ... }` record | All Roc records are structural |
| `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#:**
```fsharp
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:**
```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 with `when`
- Both are structural sum types under the hood
### Pattern 2: Result for Error Handling
**F#:**
```fsharp
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:**
```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; Roc `Err(DivByZero)` uses typed tags
- Both propagate errors up the call stack
### Pattern 3: List Operations
**F#:**
```fsharp
let result =
items
|> List.filter (fun x -> x.Active)
|> List.map (fun x -> x.Value)
|> List.sum
```
**Roc:**
```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` → Roc `keepIf` (more descriptive name)
- F# `sum` → Roc `walk(0, Num.add)` (explicit fold)
- Both use pipeline operator (`|>`) idiomatically
### Pattern 4: Pattern Matching
**F#:**
```fsharp
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:**
```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` → Roc `when` (same exhaustiveness checking)
- F# interpolation `$"{x}"` → Roc interpolation `\(x)` (different syntax)
### Pattern 5: Record Updates
**F#:**
```fsharp
type Person = {
FirstName: string
LastName: string
Age: int
}
let person = { FirstName = "Alice"; LastName = "Smith"; Age = 30 }
let olderPerson = { person with Age = 31 }
```
**Roc:**
```roc
Person : {
firstName : Str,
lastName : Str,
age : U32,
}
person = { firstName: "Alice", lastName: "Smith", age: 30 }
olderPerson = { person & age: 31 }
```
**Why this translation:**
- F# uses `with` keyword; Roc uses `&` operator
- Both create new records (copy-on-write)
- F# uses PascalCase by convention; Roc uses camelCase
### Pattern 6: Pipeline Composition
**F#:**
```fsharp
let processUser =
fetchUser
>> validateUser
>> saveUser
// Or with pipe
let result =
userId
|> fetchUser
|> validateUser
|> saveUser
```
**Roc:**
```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#:**
```fsharp
// 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:**
```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:**
1. Convert all F# exceptions to Roc `Result` types
2. Convert F# `try/with` to Roc `when ... is` pattern matching
3. Use `!` (try operator) for error propagation instead of exception bubbling
### Multiple Error Types
**F#:**
```fsharp
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:**
```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 `Result` for 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#:**
```fsharp
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:**
```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>` → Roc `Task a err` (platform-provided)
- F# `let!` → Roc `!` suffix (try operator)
- F# `Async.Parallel` → Roc `Task.sequence` (platform decides parallelism)
- F# needs `Async.RunSynchronously`; Roc `main` is already a Task
### Pure vs Effectful Code
**F#:**
```fsharp
// Pure computation
let add x y = x + y
// Effectful computation (can do I/O anywhere)
let greet name =
printfn $"Hello, {name}!"
name
```
**Roc:**
```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:**
1. Identify all F# code that does I/O
2. Restructure to separate pure logic from effects
3. Move effects to platform Task boundaries
4. Keep business logic pure
---
## Platform Architecture
### .NET Application → Roc Application + Platform
**F# (.NET Console App):**
```fsharp
[<EntryPoint>]
let main argv =
let input = Console.ReadLine()
let processed = processInput input
Console.WriteLine(processed)
0 // Return exit code
```
**Roc (Platform-based):**
```roc
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 `Task` that the platform executes
- F# can call I/O anywhere; Roc separates pure from effectful code
---
## Common Pitfalls
1. **Assuming F# mutability works in Roc**
- F# allows `mutable` keyword and `ref` cells
- Roc has no mutable variables
- **Fix:** Redesign with immutable data structures
2. **Trying to use F# exceptions**
- F# has `raise`, `try/with`, exception types
- Roc only has `Result` type
- **Fix:** Convert all exceptions to `Result` with typed errors
3. **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
4. **Using F# computation expressions freely**
- F# has `async { }`, `result { }`, `seq { }`, etc.
- Roc only has pattern matching and `!` operator
- **Fix:** Use `when ... is` and `!` for control flow
5. **Assuming type providers exist**
- F# type providers generate types at compile time
- Roc has no metaprogramming
- **Fix:** Use external code generation tools
6. **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
7. **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
8. **Expecting REPL-driven development**
- F# has F# Interactive (FSI) for REPL workflows
- Roc supports `roc repl` but it's more limited
- **Fix:** Use `expect` for inline tests instead
---
## Module System
### F# Modules/Namespaces → Roc Interfaces
**F#:**
```fsharp
// 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:**
```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# `exposes` is explicit in Roc, implicit in F#
---
## Build System
### .NET Project → Roc Application
**F# (.fsproj):**
```xml
<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:**
```roc
# main.roc - single file or multiple interfaces
app [main] {
pf: platform "https://..."
}
import Types
import Logic
main : Task {} []
main =
Logic.run
```
**Build commands:**
```bash
# 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):**
```fsharp
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:**
```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:**
```bash
# F#
dotnet test
# Roc
roc test main.roc
```
**Migration strategy:**
- Convert Expecto/xUnit/NUnit tests to Roc `expect` statements
- 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:**
1. **Metaprogramming:** F# type providers have no Roc equivalent; use external codegen
2. **Serialization:** F# has rich JSON/XML libraries; Roc relies on platform Encode/Decode abilities
3. **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#):**
```fsharp
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):**
```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#):**
```fsharp
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):**
```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#):**
```fsharp
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):**
```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>` → Roc `Task 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 examples
- `convert-elm-clojure` - Another functional language pair conversion (similar paradigm shifts)
- `lang-fsharp-dev` - F# development patterns
- `lang-roc-dev` - Roc development patterns
Cross-cutting pattern skills:
- `patterns-concurrency-dev` - Async workflows, Task model across languages
- `patterns-serialization-dev` - JSON, validation across languages
- `patterns-metaprogramming-dev` - Type providers vs code generation
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!