Safe Haskell | None |
---|---|
Language | Haskell2010 |
The Witch package is a library that allows you to confidently convert values between various types. This module exports everything you need to perform conversions or define your own. It is designed to be imported unqualified, so getting started is as easy as:
>>>
import Witch
Synopsis
- class Cast source target where
- cast :: source -> target
- from :: forall s target source. (Identity s ~ source, Cast source target) => source -> target
- into :: forall t source target. (Identity t ~ target, Cast source target) => source -> target
- class TryCast source target where
- tryCast :: source -> Either (TryCastException source target) target
- tryFrom :: forall s target source. (Identity s ~ source, TryCast source target) => source -> Either (TryCastException source target) target
- tryInto :: forall t source target. (Identity t ~ target, TryCast source target) => source -> Either (TryCastException source target) target
- newtype TryCastException source target = TryCastException source
- as :: forall s source. Identity s ~ source => source -> source
- over :: forall t source target. (Identity t ~ target, Cast source target, Cast target source) => (target -> target) -> source -> source
- via :: forall u source target through. (Identity u ~ through, Cast source through, Cast through target) => source -> target
- tryVia :: forall u source target through. (Identity u ~ through, TryCast source through, TryCast through target) => source -> Either (TryCastException source target) target
- unsafeCast :: forall source target. (HasCallStack, TryCast source target, Show source, Typeable source, Typeable target) => source -> target
- unsafeFrom :: forall s target source. (Identity s ~ source, HasCallStack, TryCast source target, Show source, Typeable source, Typeable target) => source -> target
- unsafeInto :: forall t source target. (Identity t ~ target, HasCallStack, TryCast source target, Show source, Typeable source, Typeable target) => source -> target
- liftedCast :: forall source target. (TryCast source target, Lift target, Show source, Typeable source, Typeable target) => source -> Q (TExp target)
- liftedFrom :: forall s target source. (Identity s ~ source, TryCast source target, Lift target, Show source, Typeable source, Typeable target) => source -> Q (TExp target)
- liftedInto :: forall t source target. (Identity t ~ target, TryCast source target, Lift target, Show source, Typeable source, Typeable target) => source -> Q (TExp target)
Motivation
Haskell provides many ways to convert between common types, and core
libraries add even more. It can be challenging to know which function to
use when converting from some source type a
to some target type b
. It
can be even harder to know if that conversion is safe or if there are any
pitfalls to watch out for.
This library tries to address that problem by providing a common
interface for converting between types. The Cast
type class
is for conversions that cannot fail, and the TryCast
type
class is for conversions that can fail. These type classes are inspired
by the From
trait in Rust.
Alternatives
Many Haskell libraries already provide similar functionality. How is this library different?
Coercible
: This type class is solved by the compiler, but it only works for types that have the same runtime representation. This is very convenient fornewtype
s, but it does not work for converting between arbitrary types likeInt8
andInt16
.Convertible
: This popular conversion type class is similar to what this library provides. The main difference is that it does not differentiate between conversions that can fail and those that cannot.From
: This type class is almost identical to what this library provides. Unfortunately it is part of thebasement
package, which is an alternative standard library that some people may not want to depend on.Inj
: This type class requires instances to be an injection, which means that no two input values should map to the same output. That restriction prohibits many useful instances. Also many instances throw impure exceptions.
In addition to those general-purpose type classes, there are many alternatives for more specific conversions. How does this library compare to those?
- Monomorphic conversion functions like
Data.Text.pack
are explicit but not necessarily convenient. It can be tedious to manage the imports necessary to use the functions. And if you want to put them in a custom prelude, you will have to come up with your own names. - Polymorphic conversion methods like
toEnum
are more convenient but may have unwanted semantics or runtime behavior. For example theEnum
type class is more or less tied to theInt
data type and frequently throws impure exceptions. - Polymorphic conversion functions like
fromIntegral
are very convenient. Unfortunately it can be challenging to know which types have the instances necessary to make the conversion possible. And even if the conversion is possible, is it safe? For example converting a negativeInt
into aWord
will overflow, which may be surprising.
Instances
When should you add a Cast
(or TryCast
)
instance for some pair of types? This is a surprisingly tricky question
to answer precisely. Instances are driven more by guidelines than rules.
- Conversions must not throw impure exceptions. This means no
undefined
or anything equivalent to it. - Conversions should be unambiguous. If there are multiple reasonable
ways to convert from
a
tob
, then you probably should not add aCast
instance for them. - Conversions should be lossless. If you have
Cast a b
then no twoa
values should be converted to the sameb
value. - If you have both
Cast a b
andCast b a
, thencast @b @a . cast @a @b
should be the same asid
. In other words,a
andb
are isomorphic. - If you have both
Cast a b
andCast b c
, then you could also haveCast a c
and it should be the same ascast @b @c . cast @a @b
. In other words,Cast
is transitive.
In general if s
is a t
, then you should add a Cast
instance for it. But if s
merely can be a t
, then you could add a
TryCast
instance for it. And if it is technically
possible to convert from s
to t
but there are a lot of caveats, you
probably should not write any instances at all.
Type applications
This library is designed to be used with the TypeApplications
language extension. Although it is not required for basic functionality,
it is strongly encouraged. You can use cast
,
tryCast
, unsafeCast
, and
liftedCast
without type applications. Everything else
requires a type application.
Ambiguous types
You may see Identity
show up in some type signatures. Anywhere you see
Identity a
, you can mentally replace it with a
. It is a type family
used to trick GHC into requiring type applications for certain functions.
If you forget to give a type application, you will see an error like
this:
>>>
from (1 :: Int8) :: Int16
<interactive>:1:1: error: * Couldn't match type `Identity s0' with `Int8' arising from a use of `from' The type variable `s0' is ambiguous * In the expression: from (1 :: Int8) :: Int16 In an equation for `it': it = from (1 :: Int8) :: Int16
You can fix the problem by giving a type application:
>>>
from @Int8 1 :: Int16
1
Type classes
Cast
class Cast source target where Source #
This type class is for converting values from some source
type into
some other target
type. The constraint Cast source target
measn that
you can convert from a value of type source
into a value of type
target
.
This type class is for conversions that cannot fail. If your conversion can
fail, consider implementing TryCast
instead.
Nothing
cast :: source -> target Source #
This method implements the conversion of a value between types. At call
sites you will usually want to use from
or into
instead of this
method.
The default implementation of this method simply calls coerce
,
which works for types that have the same runtime representation. This
means that for newtype
s you do not need to implement this method at
all. For example:
>>>
newtype Name = Name String
>>>
instance Cast Name String
>>>
instance Cast String Name
Instances
from :: forall s target source. (Identity s ~ source, Cast source target) => source -> target Source #
This is the same as cast
except that it requires a type
application for the source
type.
-- Avoid this: cast (x :: s) -- Prefer this: from @s x
into :: forall t source target. (Identity t ~ target, Cast source target) => source -> target Source #
This is the same as cast
except that it requires a type
application for the target
type.
-- Avoid this: cast x :: t -- Prefer this: into @t x
TryCast
class TryCast source target where Source #
This type class is for converting values from some source
type into
some other target
type. The constraint TryCast source target
means that
you may be able to convert from a value of type source
into a value of
type target
, but that conversion may fail at runtime.
This type class is for conversions that can fail. If your conversion cannot
fail, consider implementing Cast
instead.
tryCast :: source -> Either (TryCastException source target) target Source #
This method implements the conversion of a value between types. At call
sites you will usually want to use tryFrom
or tryInto
instead of this
method.
Instances
tryFrom :: forall s target source. (Identity s ~ source, TryCast source target) => source -> Either (TryCastException source target) target Source #
This is the same as tryCast
except that it requires a type
application for the source
type.
-- Avoid this: tryCast (x :: s) -- Prefer this: tryFrom @s x
tryInto :: forall t source target. (Identity t ~ target, TryCast source target) => source -> Either (TryCastException source target) target Source #
This is the same as tryCast
except that it requires a type
application for the target
type.
-- Avoid this: tryCast x :: Either (TryCastException s t) t -- Prefer this: tryInto @t x
newtype TryCastException source target Source #
This exception is thrown when a TryCast
conversion fails. It has the
original source
value that caused the failure and it knows the target
type it was trying to convert into.
TryCastException source |
Instances
Utilities
as :: forall s source. Identity s ~ source => source -> source Source #
This is the same as id
except that it requires a type application. This
can be an ergonomic way to pin down a polymorphic type in a function
pipeline. For example:
-- Avoid this: f . (\ x -> x :: Int) . g -- Prefer this: f . as @Int . g
over :: forall t source target. (Identity t ~ target, Cast source target, Cast target source) => (target -> target) -> source -> source Source #
This function converts from some source
type into some target
type,
applies the given function, then converts back into the source
type. This
is useful when you have two types that are isomorphic but some function
that only works with one of them.
-- Avoid this: from @t . f . from @s -- Prefer this: over @t f
via :: forall u source target through. (Identity u ~ through, Cast source through, Cast through target) => source -> target Source #
This function first converts from some source
type into some through
type, and then converts that into some target
type. Usually this is used
when writing Cast
instances. Sometimes this can be used to work
around the lack of an instance that should probably exist.
-- Avoid this: from @u . into @u -- Prefer this: via @u
tryVia :: forall u source target through. (Identity u ~ through, TryCast source through, TryCast through target) => source -> Either (TryCastException source target) target Source #
This is similar to via
except that it works with TryCast
instances instead. This function is especially convenient because juggling
the types in the TryCastException
can be tedious.
-- Avoid this: fmap (tryFrom @u) . tryInto @u -- Prefer this: tryVia @u
Unsafe
unsafeCast :: forall source target. (HasCallStack, TryCast source target, Show source, Typeable source, Typeable target) => source -> target Source #
This function is like tryCast
except that it will throw an
impure exception if the conversion fails.
-- Avoid this: either throw id . cast -- Prefer this: unsafeCast
unsafeFrom :: forall s target source. (Identity s ~ source, HasCallStack, TryCast source target, Show source, Typeable source, Typeable target) => source -> target Source #
This function is like from
except that it will throw an impure
exception if the conversion fails.
-- Avoid this: either throw id . from @s -- Prefer this: unsafeFrom @s
unsafeInto :: forall t source target. (Identity t ~ target, HasCallStack, TryCast source target, Show source, Typeable source, Typeable target) => source -> target Source #
This function is like into
except that it will throw an impure
exception if the conversion fails.
-- Avoid this: either throw id . into @t -- Prefer this: unsafeInto @t
Template Haskell
liftedCast :: forall source target. (TryCast source target, Lift target, Show source, Typeable source, Typeable target) => source -> Q (TExp target) Source #
This is like unsafeCast
except that it works at compile time
rather than runtime.
-- Avoid this: unsafeCast "some literal" -- Prefer this: $$(liftedCast "some literal")
liftedFrom :: forall s target source. (Identity s ~ source, TryCast source target, Lift target, Show source, Typeable source, Typeable target) => source -> Q (TExp target) Source #
This is like unsafeFrom
except that it works at compile time
rather than runtime.
-- Avoid this: unsafeFrom @s "some literal" -- Prefer this: $$(liftedFrom @s "some literal")
liftedInto :: forall t source target. (Identity t ~ target, TryCast source target, Lift target, Show source, Typeable source, Typeable target) => source -> Q (TExp target) Source #
This is like unsafeInto
except that it works at compile time
rather than runtime.
-- Avoid this: unsafeInto @t "some literal" -- Prefer this: $$(liftedInto @t "some literal")