Stream: show and tell

Topic: roc-redis


view this post on Zulip Jared Ramirez (Sep 08 2026 at 23:24):

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

view this post on Zulip Luke Boswell (Sep 08 2026 at 23:33):

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:

view this post on Zulip Richard Feldman (Sep 09 2026 at 01:25):

those numbers are haaaaawt

view this post on Zulip Richard Feldman (Sep 09 2026 at 01:26):

I wonder if you could get a more static dispatch-y API? :thinking:

view this post on Zulip Richard Feldman (Sep 09 2026 at 01:29):

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

view this post on Zulip Richard Feldman (Sep 09 2026 at 01:30):

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

view this post on Zulip Jared Ramirez (Sep 15 2026 at 13:56):

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!

view this post on Zulip Jared Ramirez (Sep 15 2026 at 13:56):

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