FP Complete is known for our best-in-class DevOps automation tooling in addition to Haskell. We use industry standards like Kubernetes and Docker. We’re always looking for new developments in software that help us empower our clients.
Rust is an up-and-coming programming language whose history starts in 2010. Rust has much overlap in its strengths with Haskell. We are impressed with Rust’s tooling, library ecosystem, and the community behind all of it. We’re also keenly interested in incorporating Rust into our work more and more.
Haskell has served FP Complete very well throughout its history, but it isn’t ideal in all circumstances. Haskell works exceptionally well as an application language, especially for web applications and networked servers. Haskell has seen productive use in everything from financial technology to non-profit web platforms. We believe Haskell excels when you want to be able to maintain quality and maintainability without compromising developer productivity.
Haskell and Rust have shared goals and design priorities. Those overlapping priorities align well with what we value at FP Complete:
Here’s an example of structures being alike in Haskell and Rust:
data Maybe a =
Nothing
| Just a
defaultOne :: Maybe Int -> Int
defaultOne Nothing = 1
defaultOne (Just n) = n
pub enum Option<T> {
None,
Some(T),
}
pub fn default_one(v: &Option<i64>) -> i64 {
match v {
None => 1,
Some(n) => *n,
}
}
You should uppercase type variables when writing Rust. In Haskell, they’re always lowercase. Lifetimes start with a single quote and are lowercase in Rust.
maybeEither :: Maybe a -> Either String a
maybeEither Nothing = Left "The value was missing!"
maybeEither (Just v) = Right v
pub fn optional_result<T>(v: Option<T>) -> Result<T, String> {
match v {
None => Err("The value was missing!".to_string()),
Some(x) => Ok(x),
}
}
Here we didn’t need to know the type of the values inside the
Maybe
or Option
type. Instead, we left
them polymorphic.
I pulled this example from the second edition of The Rust Programming Language:
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct Article {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for Article {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
Here’s my version of the example in Haskell:
import Text.Printf
class Summary a where
summarize :: a -> String
data Article =
Article {
headline :: String
, location :: String
, author :: String
, content :: String
}
instance Summary Article where
summarize (Article headline location author _) =
printf "%s, by %s (%s)" headline author location
A vacuous example to demonstrate the facility:
class Hello a where
type Return a
helloWorld :: a -> Return
trait Hello {
type Return;
fn hello_world(&self) -> Self::Return;
}
Haskell is going to be stronger when you need maximum productivity when going from a prototype to a production-ready system that can nimbly handle functional and infrastructural changes. Rust can be a better choice when you can plan a bit more and are willing to sacrifice some productivity for better performance or because your project requires a fully capable systems language.
tokio
:extern crate tokio;
use tokio::prelude::*;
use tokio::net::TcpListener;
use tokio::io::copy;
pub fn main() -> Result<(), Box<std::error::Error>> {
let addr = "127.0.0.1:3000".parse()?;
let listen_socket = TcpListener::bind(&addr)?;
let server = listen_socket
.incoming()
.map_err(|e| eprintln!("Error accepting socket: {}", e))
.for_each(|socket| {
let (reader, writer) = socket.split();
let handle_conn =
copy(reader, writer)
.map(|copy_info| println!("Finished, bytes copied: {:?}", copy_info))
.map_err(|e| {
eprintln!("Error echoing: {}", e);
})
;
tokio::spawn(handle_conn)
})
;
tokio::run(server);
Ok(())
}
Now the same in Haskell:
#!/usr/bin/env stack
-- stack --resolver lts-12.9 script
{-# LANGUAGE OverloadedStrings #-}
module Echo where
import Data.Streaming.Network (bindPortTCP)
import qualified Network.Socket as N
import qualified Network.Socket.ByteString as NB
import Control.Concurrent (forkIO)
import Control.Exception (bracket)
import Control.Monad (forever)
main :: IO ()
main = bracket
(bindPortTCP 3000 "127.0.0.1")
N.close
$ listenSocket -> forever $ do
(socket, _addr) <- N.accept listenSocket
forkIO $ forever $ do
bs <- NB.recv socket 4096
NB.sendAll socket bs
Here’s an example in Haskell using forkIOWithUnmask:
-- From https://www.fpcomplete.com/blog/2018/04/async-exception-handling-haskell
import Control.Concurrent
import Control.Exception
import System.IO
main :: IO ()
main = do
hSetBuffering stdout LineBuffering
putStrLn "Acquire in main thread"
tid <- uninterruptibleMask_ $ forkIOUnmask $ unmask ->
unmask (putStrLn "use in child thread" >> threadDelay maxBound)
`finally` putStrLn "cleanup in child thread"
killThread tid -- built on top of throwTo
putStrLn "Exiting the program"
GHC Haskell’s runtime also lets you cancel long running CPU
tasks, which is notoriously tricky elsewhere.
throwTo
and
killThread
are the most common means of doing
so.
Doing something equivalent in Rust requires writing cooperative threading behavior into the threads you want to be able to kill:
use std::thread;
use std::time::Duration;
use std::sync::mpsc::{self, TryRecvError};
use std::io::{self, BufRead};
fn main() {
println!("Acquire in main thread");
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
loop {
println!("use in child thread");
// You can't do blocking operations like this in Rust.
// It won't reach the `rx.try_recv()` or match
// thread::sleep(Duration::from_millis(std::u64::MAX));
match rx.try_recv() {
Ok(_) | Err(TryRecvError::Disconnected) => {
println!("Terminating.");
break;
}
Err(TryRecvError::Empty) => {}
}
}
});
let _ = tx.send(());
println!("Exiting the program");
}
However, this cooperation means you can’t ever block indefinitely in your Rust code, or your thread could stay stuck for the entire lifespan of your process. In GHC Haskell, your code automatically yields control to the runtime whenever it allocates memory. The strength of Haskell here is that you get the ability to preempt or kill threads without doing unspeakable violence to your code’s control flow.
Rust has some hard design limitations which are reasonable for what it prioritizes. Haskell, Java, Python, Ruby, and JS also have garbage collection. Rust does edge into the same territory, but Rust has a specific mission to be a better systems language. Targeting core systems applications means Rust is more directly comparable to C and C++. The Rust core team does their utmost to incorporate modern programming language design. The phrase the Rust team members like to use is that they’re trying to make the best 90’s era programming language they can. I think this is maybe under-selling it a little, but it’s a far sight better than the 60s and 70s vintage PL design available elsewhere.
Rust is stronger for systems programming, embedded, game development, and high-performance computing. Rust is more reliably performant than Haskell, relying less on compiler magic and more on zero-cost abstractions. This emphasis means that the designers try to introduce as much programming convenience as possible where it won’t involuntarily reduce performance. Rust’s Iterators is an excellent example of this. Haskell tries to obtain some of these benefits with the use of developer-written rewrite rules which are notoriously brittle and hard to debug.
The learning curve of both Haskell and Rust is worthwhile. They are both platforms that you can invest deeply into for robust infrastructure and applications that perform well. On top of that, their respective type systems and idioms enable developers to move faster once they’re comfortable.
Both languages and their associated ecosystems can make normal software development dramatically more tractable and scalable. If you’re interested in Haskell or Rust, please check out some of our other blog posts on these great platforms:
If you’d like help evaluating Haskell, Rust, or other technologies such as Kubernetes and Docker, please contact us! We are capable of taking your software projects from planning and scoping through project management, implementation, operations, and maintenance.
Subscribe to our blog via email
Email subscriptions come from our Atom feed and are handled by Blogtrottr. You will only receive notifications of blog posts, and can unsubscribe any time.