Getting started
Build and install
The binding is not yet published to Hackage. Clone the repository and work from
bindings/haskell/:
git clone https://github.com/apache/opendal.git
cd opendal/bindings/haskell
cabal build
A working Rust toolchain (cargo) is required alongside GHC and cabal; the
custom build step compiles the Rust FFI library automatically.
Your first program
The snippet below creates an in-memory operator (no credentials needed), writes two keys, reads them back, checks existence, inspects metadata, and deletes a key.
import OpenDAL
import qualified Data.ByteString as BS
main :: IO ()
main = do
-- newOperator accepts any IsString value; "memory" needs no credentials.
Right op <- newOperator "memory"
-- The same verbs work on every service.
Right () <- writeOpRaw op "hello.txt" "Hello, World!"
Right bytes <- readOpRaw op "hello.txt"
putStrLn $ "read " ++ show (BS.length bytes) ++ " bytes"
Right meta <- statOpRaw op "hello.txt"
putStrLn $ "size = " ++ show (mContentLength meta) ++ " bytes"
Right () <- deleteOpRaw op "hello.txt"
return ()
runOp executes the OperatorT block against the operator and returns
IO (Either OpenDALError a). Inside the block, operations use MonadOperation
methods and errors abort the block early.
Using the raw IO API
Every operation is also available as a standalone IO function returning
Either OpenDALError a. Use these when you do not want the monad transformer:
import OpenDAL
main :: IO ()
main = do
Right op <- newOperator "memory"
writeOpRaw op "key1" "value1" >>= \case
Left err -> putStrLn $ "write failed: " ++ show err
Right () -> return ()
readOpRaw op "key1" >>= \case
Left err -> putStrLn $ "read failed: " ++ show err
Right bytes -> print bytes -- "value1"
isExistOpRaw op "key1" >>= \case
Left err -> putStrLn $ "isExist failed: " ++ show err
Right doesExist -> print doesExist -- True
Handling errors
All errors carry an ErrorCode and a human-readable message:
import OpenDAL
main :: IO ()
main = do
Right op <- newOperator "memory"
result <- runOp op $ readOp "does-not-exist"
case result of
Left (OpenDALError code msg) -> do
putStrLn $ "Code: " ++ show code -- NotFound
putStrLn $ "Message: " ++ msg
Right _ -> putStrLn "unexpected success"
See the ErrorCode type for the complete set of error codes. Common ones include
NotFound, PermissionDenied, AlreadyExists, Unsupported, ConfigInvalid,
RateLimited, IsADirectory, NotADirectory, IsSameFile, FFIError, and
Unexpected.
Point it at a real backend
Pass a HashMap of service-specific config keys to OperatorConfig. The
OverloadedStrings extension (enabled by default in the cabal file) lets you
write the scheme as a string literal:
import Data.HashMap.Strict qualified as HashMap
import OpenDAL
main :: IO ()
main = do
let cfg = "s3"
{ ocConfig = HashMap.fromList
[ ("bucket", "my-bucket")
, ("region", "us-east-1")
]
}
result <- newOperator cfg
case result of
Left err -> putStrLn $ "operator creation failed: " ++ show err
Right op -> do
writeOpRaw op "hello.txt" "Hello from S3!" >>= print
Config keys match the Rust core; see Services for the full list of keys for each backend.
Streaming writes
For writing in chunks, use the Writer API instead of writeOpRaw:
import OpenDAL
main :: IO ()
main = do
Right op <- newOperator "memory"
Right writer <- writerOpRaw op "output.bin" defaultWriterOption
writerWrite writer "chunk one " -- returns Either OpenDALError ()
writerWrite writer "chunk two"
writerClose writer >>= \case -- returns Either OpenDALError Metadata
Left err -> putStrLn $ "close failed: " ++ show err
Right meta -> print (mContentLength meta) -- 19
Use appendWriterOption instead of defaultWriterOption to append to an
existing file (service-dependent; Unsupported is returned if the backend does
not support it).
Listing entries
listOpRaw returns a Lister; pull entries one at a time with nextLister
until it returns Right Nothing:
import OpenDAL
collectAll :: Lister -> IO [String]
collectAll lister = go []
where
go acc = nextLister lister >>= \case
Right (Just path) -> go (path : acc)
Right Nothing -> return (reverse acc)
Left err -> fail $ "lister error: " ++ show err
main :: IO ()
main = do
Right op <- newOperator "memory"
writeOpRaw op "dir/a.txt" "a" >>= print
writeOpRaw op "dir/b.txt" "b" >>= print
Right lister <- listOpRaw op "dir/" -- path must end with /
paths <- collectAll lister
mapM_ putStrLn paths
scanOpRaw works the same way but lists recursively (flat traversal of a
prefix).
Enable logging
Pass a co-log action in ocLogAction to see per-request log messages:
import Colog (simpleMessageAction)
import OpenDAL
main :: IO ()
main = do
Right op <- newOperator "memory" { ocLogAction = Just simpleMessageAction }
writeOpRaw op "k" "v" >>= print