Type Safety in Financial Systems: OCaml vs Rust
Comparing two languages that take type safety seriously, and why it matters for mission-critical financial applications
A bug in a financial system can cost millions. Type safety catches entire classes of bugs at compile time:
Let's compare two languages that excel at type safety: OCaml and Rust.
1(* Define currency types *)
2type usd
3type eur
4type btc
5
6(* Money type parameterized by currency *)
7type 'currency money = Money of int64
8
9(* Constructor functions *)
10let usd (cents : int64) : usd money = Money cents
11let eur (cents : int64) : eur money = Money cents
12let btc (satoshis : int64) : btc money = Money satoshis
13
14(* Type-safe operations *)
15let add_usd (Money a : usd money) (Money b : usd money) : usd money =
16 Money (Int64.add a b)
17
18(* This won't compile! *)
19(* let wrong = add_usd (usd 100L) (eur 100L) *)
20(* Error: This expression has type eur money
21 but an expression was expected of type usd money *)
221// Define currency types as zero-sized types
2struct USD;
3struct EUR;
4struct BTC;
5
6// Money type parameterized by currency
7struct Money<C> {
8 amount: i64,
9 _currency: PhantomData<C>,
10}
11
12impl Money<USD> {
13 fn dollars(amount: i64) -> Self {
14 Money {
15 amount: amount * 100, // Store as cents
16 _currency: PhantomData
17 }
18 }
19
20 fn cents(amount: i64) -> Self {
21 Money {
22 amount,
23 _currency: PhantomData
24 }
25 }
26}
27
28// Type-safe addition
29impl<C> std::ops::Add for Money<C> {
30 type Output = Money<C>;
31
32 fn add(self, other: Self) -> Self::Output {
33 Money {
34 amount: self.amount + other.amount,
35 _currency: PhantomData,
36 }
37 }
38}
39
40// This won't compile!
41// let wrong = Money::<USD>::dollars(100) + Money::<EUR>::euros(100);
42// Error: mismatched types
43Winner: Tie. Both prevent mixing currencies at compile time.
1(* No null in OCaml! Use option type *)
2type 'a option =
3 | None
4 | Some of 'a
5
6(* Finding a user *)
7let find_user (id : int) : user option =
8 match Database.query id with
9 | [] -> None
10 | u :: _ -> Some u
11
12(* Pattern matching forces handling both cases *)
13let process_user id =
14 match find_user id with
15 | None -> print_endline "User not found"
16 | Some user -> print_endline user.name
171// No null in Rust either! Use Option<T>
2enum Option<T> {
3 None,
4 Some(T),
5}
6
7// Finding a user
8fn find_user(id: u64) -> Option<User> {
9 database::query(id)
10}
11
12// Pattern matching (exhaustive)
13fn process_user(id: u64) {
14 match find_user(id) {
15 None => println!("User not found"),
16 Some(user) => println!("{}", user.name),
17 }
18}
19
20// Or use combinators
21fn get_user_email(id: u64) -> Option<String> {
22 find_user(id)
23 .map(|user| user.email)
24 .filter(|email| !email.is_empty())
25}
26Winner: Tie. Both eliminate null pointer exceptions entirely.
1(* Order states *)
2type pending
3type validated
4type executed
5type cancelled
6
7(* Order type parameterized by state *)
8type _ order =
9 | Pending : { id : string; amount : int64 } -> pending order
10 | Validated : { id : string; amount : int64; risk_approved : bool } -> validated order
11 | Executed : { id : string; amount : int64; execution_price : int64 } -> executed order
12 | Cancelled : { id : string; reason : string } -> cancelled order
13
14(* State transitions only allow valid moves *)
15let validate (Pending o : pending order) : validated order =
16 Validated {
17 id = o.id;
18 amount = o.amount;
19 risk_approved = check_risk o.amount
20 }
21
22let execute (Validated o : validated order) price : executed order =
23 Executed {
24 id = o.id;
25 amount = o.amount;
26 execution_price = price
27 }
28
29(* This won't compile! *)
30(* let wrong = execute (Pending { id = "1"; amount = 100L }) 1000L *)
31(* Error: This expression has type pending order
32 but an expression was expected of type validated order *)
331// State types
2struct Pending;
3struct Validated;
4struct Executed;
5struct Cancelled;
6
7// Order parameterized by state
8struct Order<S> {
9 id: String,
10 amount: i64,
11 state: PhantomData<S>,
12}
13
14// State-specific data
15struct ValidatedData {
16 risk_approved: bool,
17}
18
19struct ExecutedData {
20 execution_price: i64,
21}
22
23// State transitions
24impl Order<Pending> {
25 fn new(id: String, amount: i64) -> Self {
26 Order { id, amount, state: PhantomData }
27 }
28
29 fn validate(self) -> (Order<Validated>, ValidatedData) {
30 let data = ValidatedData {
31 risk_approved: check_risk(self.amount),
32 };
33 let order = Order {
34 id: self.id,
35 amount: self.amount,
36 state: PhantomData,
37 };
38 (order, data)
39 }
40}
41
42impl Order<Validated> {
43 fn execute(self, price: i64) -> (Order<Executed>, ExecutedData) {
44 let data = ExecutedData {
45 execution_price: price,
46 };
47 let order = Order {
48 id: self.id,
49 amount: self.amount,
50 state: PhantomData,
51 };
52 (order, data)
53 }
54}
55
56// This won't compile!
57// let order = Order::<Pending>::new("1".into(), 100);
58// let (executed, _) = order.execute(1000); // Error!
59Winner: OCaml. GADTs are more elegant than Rust's typestate pattern.
1(* No data races! Shared memory requires explicit synchronization *)
2let process_orders orders =
3 let results = ref [] in
4 let domains = List.map (fun order ->
5 Domain.spawn (fun () ->
6 let result = process_order order in
7 Mutex.protect mutex (fun () ->
8 results := result :: !results
9 )
10 )
11 ) orders in
12 List.iter Domain.join domains;
13 !results
141// No data races! Ownership prevents concurrent access
2fn process_orders(orders: Vec<Order>) -> Vec<Result> {
3 orders
4 .into_par_iter() // Parallel iterator
5 .map(|order| process_order(order))
6 .collect()
7}
8
9// Shared state requires explicit synchronization
10use std::sync::{Arc, Mutex};
11
12let counter = Arc::new(Mutex::new(0));
13let handles: Vec<_> = (0..10)
14 .map(|_| {
15 let counter = Arc::clone(&counter);
16 thread::spawn(move || {
17 let mut num = counter.lock().unwrap();
18 *num += 1;
19 })
20 })
21 .collect();
22
23// Won't compile without Arc + Mutex
24// let x = vec![1, 2, 3];
25// thread::spawn(move || println!("{:?}", x));
26// println!("{:?}", x); // Error: value moved
27Winner: Rust. Ownership system prevents data races at compile time.
Winner: Rust for predictable low-latency systems.
Examples:
Examples:
At NordVarg, we've used both:
OCaml for a core banking system:
Rust for risk calculations:
Both languages offer excellent type safety:
| Feature | OCaml | Rust |
|---|---|---|
| Type Safety | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| Performance | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| Concurrency | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| Ease of Use | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| Ecosystem | ⭐⭐⭐ | ⭐⭐⭐⭐ |
The right choice depends on your constraints.
Need help choosing the right language? Contact us for a consultation.
Technical Writer
NordVarg Team is a software engineer at NordVarg specializing in high-performance financial systems and type-safe programming.
Get weekly insights on building high-performance financial systems, latest industry trends, and expert tips delivered straight to your inbox.