Apply a constant to all the fields of a datatype

Viewed 104

Let's say I have the following datatype

data A = A{x::Int,y::Int,z::Int}

Is there a way to apply 0 on all the fields to get something like this :

let a = A 0 0 0

Basically not repeating 0

The goal at the end would be to use mempty from Sum Int and do something like this :

let a = myfunction mempty :: Sum Int

and Have a == A 0 0 0

Thanks !

4 Answers

There isn't one function, but you can compose Control.Monad.join with itself. You also have to unpack the 0 from mempty first, since it has type Sum Int, not Int.

let a = (join . join) A (getSum mempty)

You could then define myfunction has

myfunction = (join . join) A . getSum

and write

let a = myfunction (mempty :: Sum Int)

The more direct definition of myfunction, though, would simply be

myfunction (Sum x) = A x x x

Solution 1: a generic monoid and functor

One idea is that you can easily derive a Monoid instance, in particular, mempty, for a Generic type whose fields are all instances of Monoid:

gmempty :: (Generic a, Monoid (Rep a ())) => a
gmempty = GHC.Generics.to Data.Monoid.mempty

(That was just to illustrate; we won't need to use that binding below. This definition actually needs a type annotation on to; a complete definition can be found here in generic-data.)

However Int is not a Monoid. One solution is to generalize your type to be a Functor (which can be derived with an extension), so you can first instantiate it with a Monoid, and then back to Int using fmap. The choice of monoid doesn't matter in the end, since when you use fmap you can pass any function, including a constant one to inject any value you want. So we can choose the () monoid as a restricted default:

{-# LANGUAGE DeriveFunctor, DeriveGeneric #-}
import GHC.Generics (Generic, to)

-- Solution one

data A_ a = A a a a
  deriving (Generic, Functor, Show)

type A = A_ Int

_A0 :: A_ ()
_A0 = to mempty

myfunction :: a -> A_ a
myfunction x = fmap (const x) _A0  -- aka. (Data.Functor.$>)

example :: A
example = myfunction 0

Solution 2: The one-liner library

"Put a value in every field" is exactly what a function in one-liner does (also using generics): nullaryOp. This solution has the advantage that you don't need to generalize the A type in any way. Keeping the type of fields concrete allows you to {-# UNPACK #-} them for example.

{-# LANGUAGE DeriveGeneric, TypeFamilies, TypeApplications #-}
import GHC.Generics (Generic)
import Generics.OneLiner (nullaryOp)

-- Solution two

data A' = A' Int Int Int
  deriving (Generic, Show)

myfunction' :: Int -> A'
myfunction' x = nullaryOp @((~) Int) x  -- This requires the `(~) Int` constraint from every field: they must all be of type Int.

example' :: A'
example' = myfunction' 0

Since coincidentally Data.Default defines 0 as the default value for Int, you can use data-default-class to derive it generically with DeriveAnyClass:

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}

import Data.Default (Default(..))
import GHC.Generics (Generic(..))

data A = A { x, y, z :: Int }
  deriving (Default, Generic)

-- (def :: A) == A 0 0 0

You can make this more explicit with DerivingStrategies and enforce it with -Wmissing-deriving-strategies:

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DeriveGeneric #-}
{-# OPTIONS_GHC -Wmissing-deriving-strategies #-}

import Data.Default (Default(..))
import GHC.Generics (Generic(..))

data A = A { x, y, z :: Int }
  deriving (Default, Generic)
  deriving anyclass (Default)
  deriving stock (Generic)

-- (def :: A) == A 0 0 0

However, Data.Default is a class with no laws; it’s better used to indicate the canonical default value of a more complex configuration-like type, because its choices for primitive types are essentially arbitrary.

So if you are able to change the type a bit, you can alternatively use the generic-deriving package, in one of two ways. First, by using Sum Int as the type of the fields and deriving Semigroup and Monoid instances with generics:

{-# LANGUAGE DeriveGeneric #-}

import Generics.Deriving.Monoid

data A = A { x, y, z :: Sum Int }
  deriving (Generic)

instance Monoid A where mempty = memptydefault

instance Semigroup A where (<>) = sappenddefault

-- (mempty :: A) == A 0 0 0
-- A 1 2 3 <> A 4 5 6 == A 5 7 9

Or second—and this is my preference—by replacing the data type with a newtype over a tuple of Ints, and using DerivingVia to derive the instance via Sum Int:

{-# LANGUAGE DerivingVia #-}

newtype A = A (Int, Int, Int)
  deriving (Monoid, Semigroup) via (Sum Int, Sum Int, Sum Int)

-- (mempty :: A) == A 0 0 0
-- A 1 2 3 <> A 4 5 6 == A 5 7 9

You can recover the field names using lenses (e.g. x is _1) or by writing getters and setters manually.

One possible trick is using lens to generate a prism (which in your case will actually be an iso) between your type and a tuple type:

{-# LANGUAGE TemplateHaskell #-}

import Control.Lens

data A = A{x::Int,y::Int,z::Int}
    deriving Show
makePrisms ''A
GHCi> review _A (0,0,0)  -- Or, equivalently, _A # (0,0,0)
A {x = 0, y = 0, z = 0}

If you are only interested in filling five or less fields with mempty, you can eliminate the repetitiveness by exploiting the Monoid instance for tuples:

GHCi> import Data.Monoid
GHCi> review _A (over each getSum mempty)
A {x = 0, y = 0, z = 0}

Alternatively, if your type is made of fifteen or less fields of the same type, you can use the Applicative instances for homogeneous tuples from tuples-homogenous-h98:

GHCi> import Data.Tuple.Homogenous
GHCi> review _A (untuple3 (pure 0))
A {x = 0, y = 0, z = 0}

For a more general solution, your best bet is reaching directly for generics, as in Li-yao Xia's answer.

Related