# RedisLean A high-performance Redis client for Lean 4, built on the proven **hiredis** C library with a clean monadic interface inspired by Haskell's **Hedis**. ### Key Features - ๐Ÿ”’ **Type Safety**: Leverage Lean's type system for compile-time guarantees - ๐Ÿ“ฆ **Binary Data**: Full support for binary data including embedded NULs - ๐Ÿงฎ **Monadic Interface**: Clean composition using `do` notation - โšก **Performance**: Built on battle-tested hiredis C library - ๐Ÿ”„ **Resource Safety**: Automatic connection management and cleanup - ๐Ÿ“Š **Metrics Collection**: Built-in latency and error tracking - ๐ŸŽฏ **Extensible**: Easy to add new Redis commands and operations ## Installation For detailed installation instructions including prerequisites and platform-specific setup, see [install.md](install.md). ### Quick Start ```lean import RedisLean.Ops import RedisLean.Log open RedisLean def demo : IO Unit := do let result โ† runRedis {} do -- Basic operations set "greeting" "Hello, Redis!" let msg โ† getAs String "greeting" Log.info s!"Retrieved: {msg}" -- Type-safe operations set "counter" (42 : Nat) let count โ† getAs Nat "counter" Log.info s!"Counter: {count}" -- Set operations sadd "fruits" "apple" sadd "fruits" "banana" let size โ† scard "fruits" Log.info s!"Set size: {size}" -- Key management let exists โ† existsKey "greeting" let deleted โ† del ["greeting"] Log.info s!"Deleted {deleted} keys" match result with | Except.ok _ => Log.info "Demo completed successfully!" | Except.error e => Log.error s!"Demo failed: {e}" ``` ## Core API ### Connection Management ```lean -- Automatic connection management with default config runRedis {} computation -- With custom configuration let config : Env := { host := "127.0.0.1", port := 6379, metrics := { enabled := true } } runRedis config computation ``` ### Basic Operations ```lean -- Key-value operations set : String โ†’ ฮฑ โ†’ Redis Unit -- [Codec ฮฑ] get : String โ†’ Redis ByteArray -- Raw ByteArray getAs : (ฮฑ : Type) โ†’ String โ†’ Redis ฮฑ -- [Codec ฮฑ] del : List String โ†’ Redis UInt64 existsKey : String โ†’ Redis Bool -- Conditional set operations setnx : String โ†’ ฮฑ โ†’ Redis Unit -- Set if not exists setxx : String โ†’ ฮฑ โ†’ Redis Unit -- Set if exists -- Numeric operations incr : String โ†’ Redis Int incrBy : String โ†’ Int โ†’ Redis Int decr : String โ†’ Redis Int ``` ### Set Operations ```lean -- Redis set operations sadd : String โ†’ String โ†’ Redis UInt64 -- Add member to set sismember : String โ†’ String โ†’ Redis Bool -- Check membership scard : String โ†’ Redis UInt64 -- Get set cardinality ``` ### Data Types The library supports automatic codec-based serialization: ```lean -- String data set "user:name" "Alice" let name โ† getAs String "user:name" -- Numeric types set "user:age" (25 : Nat) set "score" (-10 : Int) set "active" true let age โ† getAs Nat "user:age" let score โ† getAs Int "score" let active โ† getAs Bool "active" -- Raw binary data let raw โ† get "binary:data" -- Returns ByteArray directly ``` ### Error Handling The Redis monad provides comprehensive error handling: ```lean -- Try-catch pattern (recommended) def safeOperation : Redis Unit := do try set "key" "value" let result โ† getAs String "key" Log.info s!"Success: {result}" catch e => Log.error s!"Redis error: {e}" -- The Redis monad is: ReaderT Env (ExceptT RedisError IO) -- Errors are automatically propagated unless caught ``` ### Metrics and Logging ```lean -- Built-in metrics collection let config : Env := { metrics := { enabled := true, latencyThresholdMs := 100 } } -- Comprehensive logging Log.info "Operation completed" Log.error "Connection failed" Log.warn "High latency detected" -- In FFI context Log.EIO.info "FFI operation started" Log.EIO.error "FFI operation failed" ``` ## Architecture The library follows a layered architecture: ``` โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ High-Level API (Ops.lean) โ”‚ โ† Redis monad operations โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ Type System (Codec.lean) โ”‚ โ† Automatic serialization โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ Redis Monad (Monad.lean) โ”‚ โ† Error handling + metrics โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ FFI Bindings (FFI.lean) โ”‚ โ† Low-level C interface โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ C Implementation (c/*.c) โ”‚ โ† hiredis integration โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ``` **Key Components:** - **`Monad.lean`**: Core Redis monad with metrics integration - **`Ops.lean`**: High-level typed operations (get, set, sadd, etc.) - **`FFI.lean`**: Low-level bindings with direct ByteArray handling - **`Codec.lean`**: Type-safe serialization for String, Nat, Int, Bool - **`Error.lean`**: Comprehensive error types and handling - **`Metrics.lean`**: Performance monitoring and latency tracking - **`Log.lean`**: Structured logging for both Redis and EIO contexts **Data Flow:** ``` Lean Types โ†’ Codec โ†’ Redis Monad โ†’ FFI โ†’ C โ†’ hiredis โ†’ Redis ``` ## FFI Layer For performance-critical applications, use the FFI layer directly: ```lean import RedisLean.FFI def directFFI : EIO RedisError Unit := do FFI.withRedis "127.0.0.1" 6379 fun ctx => do let key := String.toUTF8 "direct" let value := String.toUTF8 "data" try FFI.set ctx key value let result โ† FFI.get ctx key let retrieved := String.fromUTF8! result Log.EIO.info s!"Retrieved: {retrieved}" catch e => Log.EIO.error s!"Error: {e}" ``` ## Examples The `Examples/` directory contains comprehensive usage patterns: ### **FFI Examples** (`Examples/FFI/`) Direct FFI usage for maximum performance: - `Set.lean` - Basic set operations, NX/XX options - `Get.lean` - Key retrieval with error handling - `SAdd.lean` - Redis set operations - `Del.lean` - Key deletion and existence checking ### **Monadic Examples** (`Examples/Monadic/`) High-level monadic interface with type safety: - `Set.lean` - Type-safe operations with String, Nat, Int, Bool - `Get.lean` - Automatic codec handling and error recovery - `SAdd.lean` - Set operations with membership testing - `Del.lean` - Safe deletion with comprehensive error handling Each example follows the `ex0`/`ex1`/`ex2` pattern for progressive learning. ## Performance and Safety **Type Safety Features:** - Compile-time guarantees for Redis operations - Automatic serialization/deserialization via Codec instances - Proper error handling with the Redis monad **Performance Optimizations:** - Direct hiredis C library integration - Minimal FFI overhead - Built-in metrics for performance monitoring - Automatic connection management **Memory Safety:** - Automatic resource cleanup via `runRedis` and `FFI.withRedis` - No manual memory management required - Safe ByteArray handling for binary data ## Configuration ```lean structure Env where host : String := "127.0.0.1" port : Nat := 6379 metrics : MetricsConfig := { enabled := false } ctx : Ctx := arbitrary -- Internal FFI context structure MetricsConfig where enabled : Bool := false latencyThresholdMs : Nat := 100 ``` ## Strongly typed operations A Lean term that can be encoded as a ByteArray may be stored in Redis as the value of a key. But what about its Lean type? (Here โ€œLean typeโ€ is distinct from the Redis type. For example, in Lean we might have a Nat, whereas in Redis it may be stored as an integer represented as a string.) The Lean type information may be lost. While we can always retrieve the serialized ByteArray for the term, recovering the original Lean type require to include the type (say, its name) explicitly in the serialization. To preserve type information, the client could: decorate the Redis key with the type (e.g., key:{type}), store a companion key containing the type, or embed the term in a JSON object with a schema that records the type and other metadata. Correspondingly, the "get" APIs should be designed to align with this approach. ## Future Enhancements - broader coverage of Redis commands - richer Config options with built-in validation - more comprehensive examples - expanded monadic wrapper (to match the full FFI surface) - connection Pooling: efficient management of multiple connections - support for the cluster protocol - integration with Redis Streams - support for MULTI/EXEC transactions - pipelining: batched command execution - pure Lean: direct RESP communication over TCP sockets (no external client) - interaction with Lean proof states - strongly typed set/get operations ## The Redis Monad Transformer To interact with Redis from Lean, we wrap computations in a dedicated **Redis monad transformer**. This provides a uniform way to handle environment access and error reporting. ```lean abbrev RedisT (m : Type โ†’ Type) := ReaderT Env (ExceptT RedisError m) abbrev Result (ฮฑ : Type) := Except RedisError ฮฑ abbrev Redis (ฮฑ : Type) := RedisT IO ฮฑ ``` ### What this means - `m : Type โ†’ Type` is a *type constructor*. When we write `m ฮฑ`, it means โ€œa value of type `ฮฑ` living in the computational context `m`.โ€ This context is often described as an *effect* (e.g., `Option`, `List`, `Reader`, `IO`), but itโ€™s better understood as a computational setting or container, not necessarily a side effect. - `RedisT m ฮฑ` is a computation that: 1. has access to a Redis **environment** (`Env`), 2. may fail with a `RedisError`, and 3. eventually returns a value of type `ฮฑ` in some base monad `m`. - `Redis ฮฑ` is just `RedisT IO ฮฑ`: the concrete instantiation of the Redis monad transformer on top of `IO`. - `Result ฮฑ = Except RedisError ฮฑ` is the pure, non-monadic result type: either a successful value or an error. ### Why not just `EIO`? Leanโ€™s FFI often uses `EIO`, a built-in monad for IO computations that can fail with an error: ```lean EIO ฮต ฮฑ โ‰ก EStateM ฮต IO.RealWorld ฮฑ IO ฮฑ โ‰ก EStateM IO.Error IO.RealWorld ฮฑ ``` So `IO ฮฑ` is really `EIO IO.Error ฮฑ`. But our Redis stack expects: ```lean ExceptT RedisError IO ฮฑ โ‰ก ExceptT RedisError (EStateM IO.Error IO.RealWorld) ฮฑ ``` That is, Redis computations carry their own error type `RedisError` via `ExceptT`, instead of reusing `IO.Error`. ### Bridging the gap Even though `EIO RedisError ฮฑ` is definitionally equivalent to `ExceptT RedisError IO ฮฑ`, Lean doesnโ€™t coerce between them automatically. Thatโ€™s why we need an explicit conversion function (often called `eioToExceptT`) to move from `EIO` to our `Redis` monad stack. This explicit layering: - keeps Redis errors distinct from general IO errors, - makes error handling clearer, and - avoids confusion when mixing Leanโ€™s FFI (`EIO`) with Redis computations (`ExceptT`).