Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generically putting every component of a tuple inside Maybe

Tags:

haskell

I want a function that, given any tuple, returns a tuple of the same shape in which every component x has become Just x.

For example, maybeizeProduct ('a', 'b') should return (Just 'a',Just 'b').

maybeizeProduct should work for tuples of any (reasonable) number of components.

like image 968
danidiaz Avatar asked Jan 01 '23 05:01

danidiaz


2 Answers

This can be done with generics-sop. Some preliminary extensions and imports:

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE PartialTypeSignatures #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# OPTIONS_GHC -Wno-partial-type-signatures #-}
import Generics.SOP
import Generics.SOP.NP (trans_NP)

We define this auxiliary typeclass that relates each type with its "maybeized" version:

class (b ~ Maybe a) => Maybeized a b where
  maybeize :: a -> b

instance Maybeized a (Maybe a) where
  maybeize = Just

Here is the generic function:

maybeizeProduct ::
  forall xr xs yr ys.
  ( IsProductType xr xs,
    IsProductType yr ys,
    AllZip Maybeized xs ys
  ) =>
  xr ->
  yr
maybeizeProduct = productTypeTo @yr 
                . trans_NP (Proxy @Maybeized) (mapII maybeize) 
                . productTypeFrom @xr

productTypeFrom and productTypeTo convert tuples to and from the anonymous product representation NP used by generics-sop.

The central function is trans_NP that lets us change the "index list" of the product, where the AllZip constraint ensures that the input and output index lists have the same shape and we can perform the transformation for each element.

Putting it to work:

main :: IO ()
main = do
  print $ (maybeizeProduct ('a', 'b') :: (_, _))
  print $ (maybeizeProduct ('a', 'b', 'c') :: (_, _, _))

Because the function is so generic, we need to give some hint that we want a tuple return type.

like image 140
danidiaz Avatar answered Mar 23 '23 09:03

danidiaz


The generics-sop solution is very nice, but it's not unusual to see this done, in actual practice, with type classes and boilerplate up to some reasonable tuple size. (At least, that's how I see it done in a lot of high-quality library code, like lens.)

So, for completeness, here's a no-generics solution using a type class with an associated type family to define the maybeized type:

{-# LANGUAGE TypeFamilies #-}

class ToMaybe a where
  type AsMaybe a
  toMaybe :: a -> AsMaybe a

and a series of tedious boilerplate definitions:

instance ToMaybe (a,b) where
  type AsMaybe (a,b) = (Maybe a, Maybe b)
  toMaybe (a,b) = (Just a, Just b)
instance ToMaybe (a,b,c) where
  type AsMaybe (a,b,c) = (Maybe a, Maybe b, Maybe c)
  toMaybe (a,b,c) = (Just a, Just b, Just c)
instance ToMaybe (a,b,c,d) where
  type AsMaybe (a,b,c,d) = (Maybe a, Maybe b, Maybe c, Maybe d)
  toMaybe (a,b,c,d) = (Just a, Just b, Just c, Just d)
-- etc.

giving:

λ> toMaybe (1,3)
(Just 1,Just 3)
λ> toMaybe (1,3,4)
(Just 1,Just 3,Just 4)

Optionally, the boilerplate can be generated with Template Haskell. Due to stage restrictions, we'd need to define a module containing the class itself and the TH instance generator:

{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}

module MaybeTuplesHelper where

import Control.Monad
import Language.Haskell.TH

class ToMaybe a where
  type AsMaybe a
  toMaybe :: a -> AsMaybe a

mkToMaybeInstance n = do
  nms <- replicateM n $ newName "a"
  let -- typ: (a1, a2, a3)
      typ  = foldl appT (tupleT n) . map varT $ nms
      -- mtyp: (Maybe a1, Maybe a2, Maybe a3)
      mtyp = foldl appT (tupleT n) . map (appT (conT (mkName "Maybe")) . varT) $ nms
      -- pat: (a1, a2, a3)
      pat  = tupP . map varP $ nms
      -- mexp: (Just a1, Just a2, Just a3)
      mexp  = tupE . map (appE (conE (mkName "Just")) . varE) $ nms
  [d| instance ToMaybe $(typ) where
        type AsMaybe $(typ) = $(mtyp)
        toMaybe $(pat) = $(mexp)
    |]

and then generate the instance themselves (up to 15-tuples here) in a separate module:

{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}

import MaybeTuplesHelper

concat <$> mapM mkToMaybeInstance [2..15]

and it seems to work:

λ> :l MaybeTuples
[2 of 2] Compiling Main             ( MaybeTuples.hs, interpreted ) [flags changed]
Ok, modules loaded: Main, MaybeTuplesHelper (/scratch/buhr/stack/global-project/.stack-work/odir/MaybeTuplesHelper.o).
Collecting type info for 2 module(s) ... 
λ> toMaybe (1,2,3,4,5,"six")
(Just 1,Just 2,Just 3,Just 4,Just 5,Just "six")
like image 23
K. A. Buhr Avatar answered Mar 23 '23 09:03

K. A. Buhr