Sqids Haskell
Sqids (pronounced "squids") is a small library that lets you generate YouTube-looking IDs from numbers. It's good for link shortening, fast & URL-safe ID generation and decoding back into numbers for quicker database lookups.
Table of contents
🚀 Getting started
Installation
Sqids is available on Hackage (hackage.haskell.org/package/sqids). To install it, run:
cabal install sqids
Or using Stack:
stack install sqids
Usage
The library exposes two versions of the API;
Web.Sqids
relies on Haskell's Int
data type, whereas
Web.Sqids.Integer
uses Integer
s, which support arbitrarily large integers.
If you need to work with (i.e. encode and decode to) large numbers, it is
recommended to choose the latter option, in which case you would import the
library as:
import Web.Sqids.Integer
The Haskell standard (see here)
guarantees the range supported by Int
to have an upper bound of at least
229 - 1 (= 536,870,911). If this does not present a problem for your
use case, instead use:
import Web.Sqids
Use encode
to translate a list of non-negative integers into an ID, and
decode
to retrieve back the list of numbers encoded by an ID.
encode :: (Integral a) => [a] -> Sqids Text
decode :: (Integral a) => Text -> Sqids [a]
These functions return (monadic) values of type Sqids Int a
or Sqids Integer a
.
Calling sqids
, which uses the default configuration, or runSqids
(see below)
is the most straightforward way to extract the something
from a Sqids s something
value.
sqids :: Sqids s a -> Either SqidsError a
This gives you a value of type Either SqidsError a
, where a
is the ID in the
case of encode
. If encoding fails for some reason, then the Left
constructor
contains the error. For some use cases, directly calling
sqids
or runSqids
in this way is sufficient. Doing so in multiple locations
in your code, however, doesn't scale very well, especially when IO or other
effects are involved. In this case, the SqidsT
monad transformer
is a better choice.
Encoding
module Main where
import Web.Sqids
main :: IO ()
main =
case sqids (encode [1, 2, 3]) of
Left {} -> print "Something went wrong."
Right sqid -> print sqid
The output of this program is:
"86Rf07"
Decoding
{-# LANGUAGE OverloadedStrings #-}
module Main where
import Web.Sqids
main :: IO ()
main =
case sqids (decode "86Rf07") of
Left {} -> print "Something went wrong."
Right nums -> print nums
The output of this program is:
[1,2,3]
A note about the OverloadedStrings
language extension
decode
takes a Text
value as input. If you are not compiling with
OverloadedStrings
enabled, the "86Rf07"
string literal in the previous
example would need to be explicitly converted, using the pack
function from
Data.Text
.
import Data.Text (pack)
decode (pack "86Rf07")
Setting options
To pass custom options to encode
and decode
, use runSqids
which takes
an additional SqidsOptions
argument.
runSqids :: SqidsOptions -> Sqids s a -> Either SqidsError a
See here for available options. You can override the default values using
defaultSqidsOptions
, and the following idiom:
main =
case runSqids defaultSqidsOptions{ minLength = 24 } (encode [1, 2, 3]) of
Left {} -> print "Something went wrong."
Right sqid -> print sqid
The output of this program is:
"86Rf07xd4zBmiJXQG6otHEbe"
To set a custom alphabet:
main =
case runSqids defaultSqidsOptions{ alphabet = "mTHivO7hx3RAbr1f586SwjNnK2lgpcUVuG09BCtekZdJ4DYFPaWoMLQEsXIqyz" } (encode [1, 2, 3]) of
Left {} -> print "Something went wrong."
Right sqid -> print sqid
The output of this program is:
"oz6E9F"
Or, you can set all options at once:
main = do
let options = SqidsOptions
{ alphabet = "1234567890"
, minLength = 8
, blocklist = []
}
case runSqids options (encode [1, 2, 3]) of
Left {} -> print "Something went wrong."
Right sqid -> print sqid
The output of this program is:
"38494176"
In a more realistically sized application, calling runSqids
every time you
need to access the value returned by encode
or decode
isn't ideal. Instead,
you probably want to create your SqidsOptions
once, and then do things with
the IDs across the code without having to pass the options object along every
time. Assuming your application relies on a transformer stack to combine effects
from different monads, then this implies adding the SqidsT
transformer at
some suitable layer of the stack. Instead of sqids
and runSqids
, there are
two corresponding functions to fish out 🎣 the value from
inside of SqidsT
:
sqidsT :: Monad m => SqidsT s m a -> m (Either SqidsError a)
runSqidsT :: Monad m => SqidsOptions -> SqidsT s m a -> m (Either SqidsError a)
Below is an example where SqidsT
is used in combination with the Writer
and
IO
monads.
module Main where
import Control.Monad (forM_)
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.Class (lift)
import Control.Monad.Writer (WriterT, execWriterT, tell)
import Data.Text (Text)
import Web.Sqids
main :: IO ()
main = do
w <- sqidsT (execWriterT makeIds)
case w of
Left err -> print ("Error: " <> show err)
Right ids -> print ids
makeIds :: WriterT [Text] (SqidsT Int IO) ()
makeIds = do
liftIO $ print "Generating IDs"
forM_ [1 .. 50] $ \n -> do
sqid <- encode [n, n, n, n]
tell [sqid]
The output of this program is:
"Generating IDs"
["Q8Ac4uf3","fU9zWydl","aStUNEra","KR5zQbHB","n7qefHP0","pRkWlenI","ylr03cjE","H0V1tEjl","0rTYteaW","jQw6pcuZ","P9NfMbEk","IYvhBx6l","0vTGthaI","UXLhWExs","u52hY2FK","IjvHBv6e","pqk3lJnQ","PKNDMnEj","RJepNxTd","K15yQcHf","1c72LltW","dY4YwC0z","127FLStT","F0GBXRKm","ZDMTUa09","aFtHNir0","U4LiWBxu","oRltrlxW","1w7ULqtK","nYq5fnPa","HNVMtQjF","IRv4B26F","3wWEpjeF","oXlIrpxD","RNeTNnTN","OQJXLTbo","OAJwLube","onlgrbxt","u42vYoFH","FmGvXwKx","d84vwS0K","QuAl41fQ","H9VRtOjU","sh80jrCd","sB8CjqC3","ZKMzUJ0a","XkbEbTzD","OZJnL3bj","RGevNZTU","36WapueZ"]
Error handling
Encoding and decoding can fail for various reasons.
case runSqids options (encode numbers) of
Left SqidsNegativeNumberInInput ->
print "Negative numbers are not allowed as input."
_ ->
-- etc...
See here for possible errors.
The following is an example of how to handle errors with the help of
MonadError
s exception-handling mechanism:
module Main where
import Control.Monad (when)
import Control.Monad.Except (catchError)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Either (fromRight)
import Data.Text (unpack)
import Text.Read (readMaybe)
import Web.Sqids
putStrLn_ :: String -> SqidsT Int IO ()
putStrLn_ = liftIO . putStrLn
repl :: SqidsT Int IO ()
repl = do
input <- liftIO $ do
putStrLn "Enter a comma-separated list of non-negative integers, or \"exit\"."
putStr "> "
getLine
when (input /= "exit") $ do
case readMaybe ("[" <> input <> "]") of
Nothing ->
putStrLn_ "Invalid input: Please try again."
Just numbers ->
catchError (encode numbers >>= putStrLn_ . unpack) $ \err ->
case err of
SqidsNegativeNumberInInput ->
putStrLn_ "Only non-negative integers are accepted as input."
_ ->
putStrLn_ "Unexpected error"
repl
runRepl :: IO (Either SqidsError ())
runRepl = runSqidsT defaultSqidsOptions repl
main :: IO ()
main = fromRight () <$> runRepl
Program example output:
Options
alphabet :: Text
The alphabet used by the algorithm for encoding and decoding.
- Default value:
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789
minLength :: Int
The minimum allowed length of IDs.
blocklist :: [Text]
A list of words that must never appear in IDs.
Errors
SqidsAlphabetTooShort
The alphabet must be at least 3 characters long.
SqidsAlphabetRepeatedCharacters
The provided alphabet contains duplicate characters. E.g., "abcdefgg"
is not
a valid alphabet.
SqidsInvalidMinLength
The given minLength
value is not within the valid range.
One or more numbers in the list passed to encode
are negative. Only
non-negative integers can be used as input.
SqidsMaxEncodingAttempts
Encoding failed after too many recursive iterations. This happens if the
blocklist is too restrictive. Consider the following example:
let options = defaultSqidsOptions
{ alphabet = "abc"
, blocklist = [ "cab", "abc", "bca" ]
, minLength = 3
}
in
runSqids options (encode [0])
SqidsAlphabetContainsMultibyteCharacters
The alphabet must consist of only characters in the standard single-byte
character set.
Notes
- Do not encode sensitive data. These IDs can be easily decoded.
- Default blocklist is auto-enabled. It's configured for the most common profanity words. Create your own custom list by using the blocklist parameter, or pass an empty list to allow all words.
- Read more at https://sqids.org/haskell
API documentation
See https://hackage.haskell.org/package/sqids.
License
MIT