I have simple tuples (e.g. read from a DB) from that I do not know the number of elements nor the content. E.g.
(String, Int, Int)
or (String, Float, String, Int)
.
I want to write a generic function that would take all sort of tuples and replace all data with the string "NIL". If the string "NIL" is already present it should stay untouched.
Coming back to the example:
("something", 3, 4.788)
should result in ("something", "NIL", "NIL")
("something else", "Hello", "NIL", (4,6))
should result in ("something else", "NIL", "NIL", "NIL")
I have obviously no idea where to start since it won't be a problem to do this with tuples that are known. Is it possible here to come to my desired result without Template Haskell?
Once a tuple is created, you cannot change its values. Tuples are unchangeable, or immutable as it also is called.
Ordered collections of arbitrary objects. Like strings and lists, tuples are an ordered collection of objects; like lists, they can embed any kind of object.
You can use a Tuple to store the latitude and longitude of your home, because a tuple always has a predefined number of elements (in this specific example, two). The same Tuple type can be used to store the coordinates of other locations.
Just like list data structure, a tuple is homogenous. Therefore, a tuple can consist of elements of multiple data types at the same time. You can create a tuple by placing all elements inside the parentheses(()) separated by commas.
It's possible using GHC.Generics
, I thought I'd document it here for completeness though I wouldn't recommend it over the other recommendations here.
The idea is to convert your tuples into something that can be pattern matched on. The typical way (which I believe HList
uses) is to convert from a n-tuple to nested tuples: (,,,)
-> (,(,(,)))
.
GHC.Generics
does something similar by converting the tuples to nested applications of the product :*:
constructor. to
and from
are functions that convert a value to and from their generic representation. The tuple fields are generically represented by K1
newtypes, so what we want to do is recurse down through the tree of metadata (M1
) and product (:*:
) nodes until we find the K1
leaf nodes (the constants) and replace their contents with a "NIL" string.
The Rewrite
type function describes how we're modifying the types. Rewrite (K1 i c) = K1 i String
states that we're going to replace each value (the c
type parameter) with a String
.
Given a little test app:
y0 :: (String, Int, Double)
y0 = ("something", 3, 4.788)
y1 :: (String, String, String, (Int, Int))
y1 = ("something else", "Hello", "NIL", (4,6))
main :: IO ()
main = do
print (rewrite_ y0 :: (String, String, String))
print (rewrite_ y1 :: (String, String, String, String))
We can use a generic rewriter to produce:
*Main> :main ("something","NIL","NIL") ("something else","NIL","NIL","NIL")
Using the built-in Generics
functionality and a typeclass to do the actual transformation:
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
import Data.Typeable
import GHC.Generics
rewrite_
:: (Generic a, Generic b, Rewriter (Rep a), Rewrite (Rep a) ~ Rep b)
=> a -> b
rewrite_ = to . rewrite False . from
class Rewriter f where
type Rewrite f :: * -> *
rewrite :: Bool -> f a -> (Rewrite f) a
instance Rewriter f => Rewriter (M1 i c f) where
type Rewrite (M1 i c f) = M1 i c (Rewrite f)
rewrite x = M1 . rewrite x . unM1
instance Typeable c => Rewriter (K1 i c) where
type Rewrite (K1 i c) = K1 i String
rewrite False (K1 x) | Just val <- cast x = K1 val
rewrite _ _ = K1 "NIL"
instance (Rewriter a, Rewriter b) => Rewriter (a :*: b) where
type Rewrite (a :*: b) = Rewrite a :*: Rewrite b
rewrite x (a :*: b) = rewrite x a :*: rewrite True b
And a few instances unused by this example, they'd be required for other data types:
instance Rewriter U1 where
type Rewrite U1 = U1
rewrite _ U1 = U1
instance (Rewriter a, Rewriter b) => Rewriter (a :+: b) where
type Rewrite (a :+: b) = Rewrite a :+: Rewrite b
rewrite x (L1 a) = L1 (rewrite x a)
rewrite x (R1 b) = R1 (rewrite x b)
With a bit more effort the Typeable
constraint could be removed from the K1
instance, whether it's better or not is arguable due to Overlapping/UndecidableInstances. GHC also can't infer the result type, though it's seems like it should be able to. In any case, the result type needs to be correct or you'll get a hard to read error message.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With