packages feed

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

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

{-# OPTIONS_GHC -Wno-orphans #-}

module Data.Tuple.Classes (
    -- | Generalized tuple-related utilities:
    --
    -- * 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 1-tuple.
    --
    -- === __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 f)
    -- @
    --
    -- 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 f0)’
    --
    -- 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(..),
    uncurriedN,
    uncurriedN',

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

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

import Data.Strict.Tuple qualified as Strict
import Data.Tuple        (Solo(..))
import GHC.TypeNats      (Nat)
#if defined(LENS)
import Control.Lens
#else
import Data.Functor.Identity (Identity(..))
import Lens.Micro
#endif

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
    --
    -- Instances defined in this library ensure, given a function @cur _ _ ... = k@ which ignores its arguments, that @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
    --
    -- Instances defined in this library ensure, given a function @cur _ _ ... = k@ which ignores its arguments, that @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

-- | Operate on an @n@-ary function as a unary function taking a lazy tuple argument.
uncurriedN :: forall n c d. (UncurryN n c, UncurryN n d) =>
#if defined(LENS)
    Iso c d (UncurriedN n c) (UncurriedN n d)
uncurriedN = iso (uncurryN @n) (curryN @n)
#else
    Lens c d (UncurriedN n c) (UncurriedN n d)
uncurriedN f = fmap (curryN @n) . f . (uncurryN @n)
#endif

-- | Same, but the tuple is strict.
uncurriedN' :: forall n c d. (UncurryN n c, UncurryN n d) =>
#if defined(LENS)
    Iso c d (UncurriedN' n c) (UncurriedN' n d)
uncurriedN' = iso (uncurryN' @n) (curryN' @n)
#else
    Lens c d (UncurriedN' n c) (UncurriedN' n d)
uncurriedN' f = fmap (curryN' @n) . f . (uncurryN' @n)
#endif

-- | 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@.
--
-- We do not provide an instance for inserting into the zero-tuple @()@, because the strictness is ambiguous.
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'
    --
    -- Instances defined in this library behave differently based on strictness.
    -- Given a function @cur _ _ ... = k@ which ignores its arguments, we ensure:
    --
    -- +--------------+----------------------------------------+
    -- | Lazy tuple   | @`ith` \@i \(tupleInsert \@i x ⊥) = x@ |
    -- +--------------+----------------------------------------+
    -- | Strict tuple | @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 purpose-built tuple field accessors, consider `_1` and the like.
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])