Skip to content
Back to skills

Convert Fsharp Roc

ASecurity

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.

  • 8 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added February 8, 2026
developmentgobashexpresstestingrefactoringgitapi

Works with

  • cli
  • api

Security analysis

A100/100

Scanned February 12, 2026

npx -y skills add aRustyDev/ai --skill convert-fsharp-roc --agent claude-code

Installs 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 with every re-scan.

Security grade badge for Convert Fsharp Roc
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/arustydev-convert-fsharp-roc/badge)](https://www.skillsdirectory.com/skills/arustydev-convert-fsharp-roc)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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 | `{ ... }` 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

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…