{-# OPTIONS_GHC -Wall #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeFamilies #-}

{- |
Module      : CurryTF
Description : Provides curry/uncurry-like function for any number of parameters
Copyright   : (c) David James, 2020
License     : BSD3
Stability   : Experimental

A generalisation of 'curry' and 'uncurry' , allowing currying of any number of arguments of different types.


For the class instances provided here, the arguments are packaged into a "stacked tuple".
For example @(\'x\', (3 :: Int, (True, ())))@ represents a set of three arguments of different types:

- @\'x\' :: Char@;
- @3 :: Int@; and
- @True :: Bool@.

The TF stands for Type Family. I've given this the (possibly weird) name to avoid any conflict with similar implementations.
-}

module CurryTF
  (
  -- * Class
    CurryTF(..)
  , ($#)

  -- * Stacking Helpers
  -- $StackingFunctions
  , App1, App2, App3, App4
  , app1, app2, app3, app4

  -- * Custom CurryTF Implementations
  -- $CustomArgApp

  -- * Other Implementations
  -- $SeeAlso
  )
where

{- |
Given:

- a type 'args' containing n embedded arguments; and 
- a result type 'r'

@CurryTF args r@ represents the ability to convert either way between functions:

- @fCurried :: /each/ -> /argument/ -> /as/ -> /a/ -> /separate/ -> /parameter/ -> r@; and
- @fUncurried :: /all-arguments-embedded-in-a-single-parameter/ -> r@.

so that:

- @fCurried = curryN fUncurried@; and
- @fUncurried = uncurryN fCurried@.
-}

class CurryTF args r where
  {- |
    The type of the (curried) function that can have arguments of the types embedded in 'args' applied and that returns a result of type 'r'.
    For example:

    >>> :kind! FnType (Char, (Int, (Bool, ()))) String
    FnType (Char, (Int, (Bool, ()))) String :: *
    = Char -> Int -> Bool -> [Char]
  -}
  type FnType args r :: *

  {- |
    Embeds a number of separate arguments into a single 'args' parameter, applies 'args' to a function, and returns the result.

    For example:

    >>> fn1 (c, (n, (b, ()))) = c : replicate n '1' ++ if b then "hello" else "goodbye"
    >>> curryN fn1 'x' 3 True
    "x111hello"
    
    This also support partial application:
    
    >>> :t curryN fn1 'x'
    curryN fn1 'x' :: Int -> Bool -> [Char]
  -}

  curryN :: (args -> r) -> FnType args r

  {- |
    Applies each argument embedded in 'args' as a separate parameter to a function, and returns the result.

    For example:

    >>> fn2 c n b = c : replicate n '2' ++ if b then "hello" else "goodbye"
    >>> uncurryN fn2 ('x', (3, (True, ())))
    "x222hello"
  -}
  uncurryN :: FnType args r -> args -> r

-- | the application of zero arguments, giving @r@
instance CurryTF () r where
  type FnType () r = r
  curryN f = f ()
  uncurryN f () = f

-- | the application of @arg@, followed by the application of @moreArgs@ (recursively), giving @r@
instance CurryTF moreArgs r => CurryTF (arg, moreArgs) r where
  type FnType (arg, moreArgs) r = arg -> FnType moreArgs r
  curryN f a = curryN (\t -> f (a, t))
  uncurryN f (arg, moreArgs) = uncurryN (f arg) moreArgs

-- | A binary operator for 'uncurryN', so if values a, b and c are embedded in @args@ then @f $# args = f a b c@
($#) :: CurryTF args r => FnType args r -> args -> r
f $# args = uncurryN f args

{- $StackingFunctions
These types and functions can make code that uses the "stacked tupples" look a little less weird. For example, you can write:

>>> fn2 $# app3 'x' 3 True

instead of

>>> fn2 $# ('x', (3, (True, ())))

Although these are only provided here for 1 to 4 arguments, you can use the "stacked tuple" to apply any number of arguments.
-}
{-# INLINABLE app1 #-}
{-# INLINABLE app2 #-}
{-# INLINABLE app3 #-}
{-# INLINABLE app4 #-}

type App1       a =             (a, ())
-- ^ A "stacked tuple" of one value
type App2     a b =         (a, (b, ()))
-- ^ A "stacked tuple" of two values
type App3   a b c =     (a, (b, (c, ())))
-- ^ A "stacked tuple" of three values
type App4 a b c d = (a, (b, (c, (d, ()))))
-- ^ A "stacked tuple" of four values

app1 ::                a -> App1       a
-- ^ stacks one value
app2 ::           a -> b -> App2     a b
-- ^ stacks two values
app3 ::      a -> b -> c -> App3   a b c
-- ^ stacks three values
app4 :: a -> b -> c -> d -> App4 a b c d
-- ^ stacks four values

app1       a =             (a, ())
app2     a b =         (a, (b, ()))
app3   a b c =     (a, (b, (c, ())))
app4 a b c d = (a, (b, (c, (d, ()))))

{- $CustomArgApp
It is possible to define instances for other types, for example:

@
data MyStuff = MyStuff Char Int Bool

instance CurryTF MyStuff r where
  type FnType MyStuff r = Char -> Int -> Bool -> r
  curryN f c n b = f (MyStuff c n b)
  uncurryN f (MyStuff c n b) = f c n b
@

then:

>>> fn2 $# MyStuff 'y' 5 False
"y22222goodbye"
>>> fn3 (MyStuff c n b) = c : show n ++ show b
>>> curryN fn3 'p' 8 False
"p8False"

Doing this, especially for a type with multiple constructors, may not be sensible.
-}

{- $SeeAlso
There are similar implementations in:

1. [Data.Tuple.Curry](https://hackage.haskell.org/package/tuple/docs/Data-Tuple-Curry.html); and
1. [Data.Tuple.Ops](https://hackage.haskell.org/package/tuple-sop/docs/Data-Tuple-Ops.html).

These both take tuples of the form (arg1, arg2, .., argn), which is arguably easier to use.

I built this (instead of using those), for good and bad reasons including:

- I'm trying to improve my Haskell. TypeFamilies seemed to help here, so I got to start using those too.
- (1) has a limit of 32 args. OK that's probably enough, but it just seemed wrong to have any restriction.
- (2) Seems a little complex, and excesive for the needs here. (Though, from what I've read so far, the "stacked-tuples" here are in SOP form?). They also have a limit - in this case 10 args.
-}