Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Manipulating "arbitrary" tuples

Tags:

haskell

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?

like image 928
J Fritsch Avatar asked Nov 18 '12 00:11

J Fritsch


People also ask

How do you manipulate a tuple?

Once a tuple is created, you cannot change its values. Tuples are unchangeable, or immutable as it also is called.

Can tuple include arbitrary objects?

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.

Where are tuples used in real life?

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.

How do you get a list value's tuple form?

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.


1 Answers

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.

like image 53
Nathan Howell Avatar answered Oct 18 '22 10:10

Nathan Howell