packages feed

tuple-classes-1.0.0: src/Data/Tuple/Classes.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE ImplicitParams #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}

{-# OPTIONS_GHC -Wno-orphans #-}

module Data.Tuple.Classes (
    -- | We present utilities here for generalized handling of tuples:
    --
    -- * inserting and removing fields
    -- * converting /n/-ary functions to unary and vice versa
    -- * re-exports of smaller tuples
    -- * larger strict tuples with relevant instances
    --
    -- We consider `Identity` the canonical strict tuple insofar as it adds no extra laziness.
    --
    -- === __Discussion__
    --
    -- Tuples are the way to anonymously package up potentially heterogeneous values as a single value in Haskell.
    -- (We say /anonymously/ because they have unnamed fields and thus suffer from a kind of semantic blindness.
    -- Consider defining a proper record type if you are not simply solving a localized problem of needing a single value.)
    --
    -- Unlike in Python for example, we cannot freely convert between a tuple and other types in Haskell.
    -- The issue boils down to differently-sized tuples having different numbers of type parameters.
    -- Even to access the third field of a tuple of arbitrary size, we must write out (or generate, as we do here) separate implementations covering every tuple size up to some finite number (here, 9), and thread however many type and data parameters through each definition.
    -- The best we can do to avoid an API with sprawling identifiers is offer multi-param typeclasses that can accomodate all these variables.
    --
    -- An important use case for generalized tupling\/untupling is conversion between functions of one argument and those of /n/ arguments.
    -- Functions are curried, which means that every function really takes one argument and has another function as its result.
    -- As a type construction, a function of multiple arguments is a nested type:
    --
    -- >   a ->      b ->      c ->      d ->   e
    -- >   a ->   (  b ->   (  c ->   (  d ->   e)))
    -- > ((->) a) (((->) b) (((->) c) (((->) d) e)))
    --
    -- It is not possible to write a type-level function to plumb these layers automatically and emit a single type.
    -- We /could/ write a closed type family requiring @UndecidableInstances@:
    --
    -- @
    -- type family Arity f :: Nat where
    --     Arity (a -> b) = 1 + Arity b
    --     Arity a        = 0
    --
    -- arity :: (KnownNat (Arity f)) => f -> Natural
    -- arity _ = natVal $ Proxy @(Arity a)
    -- @
    --
    -- but this falls short.
    -- If the final type is a variable, GHC cannot prove that it is not a function.
    --
    -- >>> arity (++)
    -- 2
    -- >>> arity print
    -- 1
    -- >>> arity id
    --         • No instance for ‘KnownNat (1 + Arity a0)’
    --
    -- For more food for thought, see [this sketch](https://bin.mangoiv.com/note?id=d204d07f-6292-4eb7-9aff-4cabc78394b9) for Haskell keyword arguments.
    --
    -- Even if this type family were possible, the emitted type still could not be a simple tuple without the support of a typeclass and rote instances, for the reason listed above.

    -- * Resizing tuples
    TupleAt(..),
    ith,

    -- * Generalized uncurrying
    UncurryN(..),

    -- * Re-exports
    Solo(..),
    Identity(..),
    Strict.Pair(..),
    type (Strict.:!:),

    -- * Additional strict tuples
    StrictTuple3(..),
    StrictTuple4(..),
    StrictTuple5(..),
    StrictTuple6(..),
    StrictTuple7(..),
    StrictTuple8(..),
    StrictTuple9(..),
) where

import Data.Functor.Identity (Identity(..))
import Data.Strict.Tuple     qualified as Strict
import Data.Tuple            (Solo(..))
import GHC.TypeNats          (Nat)

import Data.Tuple.Classes.TH

-- | A relation between an @n@-ary function @c@ and unary functions of lazy and strict tuple arguments, along with methods to convert between them.
--
-- Use methods with @TypeApplications@/@DataKinds@:
--
-- >>> f = printf "The %s br%dwn fo%c\n" :: String -> Int -> Char -> IO ()
-- >>> mapM_ (uncurryN @3 f) [("quick", 0, 'x'), ("employee", -86, '臭')]
-- The quick br0wn fox
-- The employee br-86wn fo臭
class UncurryN (n :: Nat) c where
    -- | A unary function isomorphic to @c@.
    -- The argument is a lazy tuple.
    type UncurriedN n c
    -- | Same, but the tuple is strict.
    type UncurriedN' n c
    -- | Convert a function taking @n@ arguments to a function taking a single lazy @n@-tuple.
    --
    -- > uncurryN :: (a -> b -> ... -> r) -> (a, b, ...) -> r
    --
    -- If the function @cur _ _ ... = k@ ignores its arguments, then @uncurryN cur ⊥ = k@.
    uncurryN :: c -> UncurriedN n c
    -- | Convert a function taking a single lazy @n@-tuple to a function taking @n@ arguments.
    --
    -- > curryN :: ((a, b, ...) -> r) -> a -> b -> ... -> r
    curryN :: UncurriedN n c -> c
    -- | Convert a function taking @n@ arguments to a function taking a single strict @n@-tuple.
    --
    -- > uncurryN' :: (a -> b -> ... -> r) -> StrictTupleN a b ... -> r
    --
    -- If the function @cur _ _ ... = k@ ignores its arguments, then @uncurryN' cur ⊥ = k@.
    uncurryN' :: c -> UncurriedN' n c
    -- | Convert a function taking a single strict @n@-tuple to a function taking @n@ arguments.
    --
    -- > curryN' :: (StrictTupleN a b ... -> r) -> a -> b -> ... -> r
    curryN' :: UncurriedN' n c -> c

-- | Inserting and removing tuple fields.
-- Specify @i@ via @TypeApplications@/@DataKinds@.
--
-- * @i@: the 1-based index of the inserted or removed field.
-- * @a@: the type of the inserted or removed field.
-- * @t@: the narrower tuple \(to be inserted into or resulting from a removal).
-- * @u@: the wider tuple \(resulting from an insertion or from which to remove something).
--
-- > tupleRemove @i . uncurry (tupleInsert @i) = id
-- > uncurry (tupleInsert @i) . tupleRemove @i = id
--
-- The valid range of @i@ is \[1, /n/], where /n/ is the width of @u@.
class TupleAt (i :: Nat) a t u | i a t -> u, i u -> a t where
    -- | Insert an @a@ into @t@ at @i@ and return the widened tuple @u@.
    -- Upon insertion, any fields at indices >=@i@ will slide to the right to make room.
    --
    -- >>> tupleInsert @3 42.0 ("Hello", (), True)
    -- ("Hello",(),42.0,True)
    -- >>> tupleInsert @2 'x' (Identity 67)
    -- 67 :!: 'x'
    --
    -- For lazy tuples, @`ith` \@i \(tupleInsert \@i x ⊥) = x@.
    --
    -- For strict tuples, @ith \@i \(tupleInsert \@i x ⊥) = ⊥@.
    tupleInsert :: a -> t -> u
    -- | Remove an @a@ from @u@ at @i@ and return both it and the remaining tuple @t@.
    -- Upon removal, any fields at indices >@i@ will slide to the left to close the gap.
    --
    -- >>> tupleRemove @1 ("Hello", (), 42.0, True)
    -- ("Hello",((),42.0,True))
    tupleRemove :: u -> (a, t)

-- | Access the @i@th field of a tuple.
-- Generalization of @fst@/@snd@.
--
-- >>> ith @1 (Foo, "bar")
-- Foo
--
-- This is a convenience function defined as @fst . tupleRemove \@i@.
-- For typeclasses purpose-built for accessing tuple fields, consider [lens\'s @Field1@ and the like](https://hackage-content.haskell.org/package/lens-5.3.6/docs/Control-Lens-Tuple.html).
-- We do not wish to duplicate that here; it is already duplicated by (the excellent) [microlens](https://hackage-content.haskell.org/package/microlens-0.5.0.0/docs/Lens-Micro-FieldN.html).
ith :: forall i a t u. (TupleAt i a t u) => u -> a
ith = fst . tupleRemove @i

$(foldMap (\n -> let ?n = n in makeStrictTupleAndInsts) [3 .. 9])
$(mapM    (\n -> let ?n = n in makeUncurryNInst)        [0 .. 9])
$(foldMap (\n -> let ?n = n in makeTupleAtInsts)        [2 .. 9])