Record of Arrays -> Array of Records

Cross-post from Slack:

Is there a recommended technique to convert from a Record of Arrays to an Array of Records? An assumption is that all arrays in MyRec1 are the same length. Not sure there’s a way to enforce this with types. Would also be happy to just have the length of the resulting array be the length of the smallest input array, or produce an error if there’s a mismatch of input lengths.

type MyRec1 =
  { a :: Array Int
  , b :: Array Int
  , c :: Array Int
  }

type MyRec2 =
  { a :: Int
  , b :: Int
  , c :: Int
  }

myConversion :: MyRec1 -> Array MyRec2
myConversion = todo

Answer:

Use sequenceRecord on a structure with a “zippy” Applicative instance. Arrays are too “explosive”, but ZipList will work. A potential todo is to contribute ZipArray to Data.Array, but for now we’ll just convert with the toZ function below. hmap applies toZ to each record field before the record is “sequenced” and then converted back into an array.

import Data.Array as A
import Data.List.Lazy as LL
import Data.List.ZipList (ZipList(..))
import Heterogeneous.Mapping (hmap)
import Record.Extra (sequenceRecord)

toZ :: Array Int -> ZipList Int
toZ = LL.fromFoldable >>> ZipList

myConversion :: MyRec1 -> Array MyRec2
myConversion =
  hmap toZ >>> sequenceRecord >>> A.fromFoldable

If the input record types are heterogeneous rather than homogeneous (i.e. not all Array Int) such as below:

type MyRec1Het =
  { a :: Array Int
  , b :: Array Number
  , c :: Array Int
  }

type MyRec2Het =
  { a :: Int
  , b :: Number
  , c :: Int

It is tempting to generalize the toZ function to:

toZHet :: forall a. Array a -> ZipList a
toZHet = LL.fromFoldable >>> ZipList

However this won’t work, and is explained in more detail in the purescript-heterogeneous docs.

The solution is this approach:

data ToZHet = ToZHet

instance toZHet :: Mapping ToZHet (Array a) (ZipList a) where
  mapping _ = LL.fromFoldable >>> ZipList

myConversionHet :: MyRec1Het -> Array MyRec2Het
myConversionHet =
  hmap ToZHet >>> sequenceRecord >>> A.fromFoldable

Testing:

rin :: MyRec1
rin =
  { a: [0, 1]
  , b: [0, 1]
  , c: [0, 1]
  }

rinHet :: MyRec1Het
rinHet =
  { a: [0, 1]
  , b: [0.0, 1.0]
  , c: [0, 1]
  }

main :: Effect Unit
main = do
  log $ show rin
  log $ show $ myConversion rin
  log $ show rinHet
  log $ show $ myConversionHet rinHet

Output:

{ a: [0,1], b: [0,1], c: [0,1] }
[{ a: 0, b: 0, c: 0 },{ a: 1, b: 1, c: 1 }]
{ a: [0,1], b: [0.0,1.0], c: [0,1] }
[{ a: 0, b: 0.0, c: 0 },{ a: 1, b: 1.0, c: 1 }]
2 Likes

If the input array sizes are known at runtime, then sized-vectors may be used instead of a ZipList.