i wanted to play around with what it looks like to create platform-agnostic libraries that rely on effects, but super pared down (after some comments in this thread #ideas > Platform extensibility using bundle of effects pattern ). the result of that is https://github.com/jaredramirez/roc-redis
all it needs from the platform is the ability to read & write bytes. all you need to give roc-redis is a way to read and write bytes, eg with basic-cli or basic-webserver:
stream = Tcp.connect!("127.0.0.1", 6379, 2_000)
? |error| ExampleFailed("connect: ${Str.inspect(error)}")
transport : Execute.Transport(_, _)
transport = {
read!: |max_bytes|
stream.read_up_to!(max_bytes, 2_000)
.map_ok(|bytes| if bytes.is_empty() End else Data(bytes)),
write_all!: |bytes| stream.write!(bytes, 2_000),
}
pong = Execute.request!(config, Commands.Connection.ping({}), transport)
? |_| ExampleFailed("Redis PING failed")
i'd be curious if there any other other platforms outside basic-* that expose similar tcp primitives, to see if this approach works well in other situations!
perf compared to other languages is pretty good (in toy benchmarks as least):
(higher is better, operations per sec)
| Client | PING | SET + GET | INCR | Pipelined PING |
|---|---|---|---|---|
| Roc / speed | 14,694 | 7,252 | 14,657 | 972,006 |
| Python / redis-py | 12,488 | 5,958 | 12,425 | 357,869 |
| Go / go-redis | 13,619 | 6,765 | 13,680 | 1,010,951 |
| Rust / redis-rs | 14,593 | 7,283 | 14,530 | 1,148,545 |
| C / hiredis | 14,506 | 7,313 | 14,528 | 1,147,183 |
Jared Ramirez said:
i'd be curious if there any other other platforms outside basic-* that expose similar tcp primitives, to see if this approach works well in other situations!
We should be able to make that happen easily enough :grinning_face_with_smiling_eyes:
those numbers are haaaaawt
I wonder if you could get a more static dispatch-y API? :thinking:
like where you could say like conn.set!(...) and conn.get!(...) and conn is something you get by calling something like Redis.connect!(...) and passing it whatever I/O stuff it needs
so that initial connection function both performs the initial connection itself and also saves the I/O operations you passed it for later, then returns you back something that is both already connected and knows how to do future I/O operations - so since you're naturally passing that value around anyway to know what your connection is, it just already has the I/O operations available on it as methods too
I've done some cleanup and some performance improvements and iterated on the API! I've landed here:
app [main!] {
pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.23.0-rc1/3hT3SoHZ6qbEsa9qVFLUW3547U5LeoNd1KbpqLpz4r1i.tar.zst",
redis: "https://github.com/jaredramirez/roc-redis/releases/download/0.1.0-rc4/8BsZJKf5j58G6rxgggefDWqmgfdDkYmJbNUSTb8NvRfC.tar.zst",
}
import pf.Tcp
import pf.Stdout
import pf.OsStr exposing [OsStr]
import redis.Bytes
import redis.Client
import redis.Commands
import redis.Config
import redis.ByteIo
# Create a redis client. Client contains many fields with default values, but
# since we are not overriding any of those values, we just leave it empty.
client = Client.{}
main! : List(OsStr) => Try({}, [ExampleFailed(Str), Exit(I32), ..])
main! = |_args| {
# Create a TCP stream
stream = Tcp.connect!("127.0.0.1", 6379, 2_000)
? |error| ExampleFailed("connect: ${Str.inspect(error)}")
# Create a ByteIo that reads and writes over the stream
byte_io = ByteIo.from_empty_eof({
read_bytes!: |max_bytes| stream.read_up_to!(max_bytes, 2_000),
write_all!: |bytes| stream.write!(bytes, 2_000),
})
# Create a re-usable connection that knows how to do IO via the transport
connection = client.connect!(byte_io)
? |_| ExampleFailed("Redis handshake failed")
# Read a value
stored = connection.request!(Commands.Strings.get("example:greeting"))
? |error| ExampleFailed("GET: ${Str.inspect(error)}")
greeting = match stored {
Present(bytes) => bytes.to_utf8() ? |_| ExampleFailed("greeting is not UTF-8")
Absent => "Hello"
}
# Update a value
updated = Bytes.from_str("${greeting}!")
_ = connection.request!(
Commands.Strings.set("example:greeting", updated, { expiration: Seconds(60) }),
) ? |error| ExampleFailed("SET: ${Str.inspect(error)}")
Stdout.line!("${greeting}!") ? |error| ExampleFailed("stdout: ${Str.inspect(error)}")
Ok({})
}
I also created a release candidate so if anyone uses this and has feedback, feel free to open issues, ping me, etc!
current numbers (operations per second, higher is better):
| Experiment | roc-redis | redis-py | go-redis | redis-rs | hiredis (c) |
|---|---|---|---|---|---|
ping_sequential |
14,836 | 12,475 | 13,839 | 14,766 | 14,728 |
set_get_sequential |
7,362 | 6,079 | 6,909 | 7,293 | 7,309 |
incr_sequential |
14,683 | 12,578 | 13,831 | 14,568 | 14,598 |
ping_pipeline |
982,800 | 358,179 | 1,050,813 | 1,207,349 | 1,226,768 |
mset_mget_sequential |
7,097 | 5,758 | 6,817 | 7,303 | 7,374 |
hash_roundtrip_sequential |
7,180 | 5,963 | 6,887 | 7,283 | 7,296 |
set_get_pipeline |
447,828 | 119,411 | 439,952 | 467,049 | 475,692 |
Last updated: Sep 24 2026 at 15:59 UTC