1
votes

I have a function that looks like this:

import Data.Bifunctor (lmap)
import Data.Int (fromString)

keyToInt :: forall a. Tuple String a -> Tuple (Maybe Int) a
keyToInt = lmap fromString 

What I'd like is this type signature:

keyToInt :: forall a. Tuple String a -> Maybe (Tuple Int a)

This seems to work:

keyToInt :: forall a. Tuple String a -> Maybe (Tuple Int a)
keyToInt tuple = extractFstMaybe $ lmap fromString tuple
  where
    extractFstMaybe :: forall a. Tuple (Maybe Int) a -> Maybe (Tuple Int a)
    extractFstMaybe (Tuple (Just a) b) = Just $ Tuple a b
    extractFstMaybe (Tuple Nothing _) = Nothing

But it feels like there should be a more abstract/cleaner way to express this. If I were to take extractMaybe to it's natural conclusion, I might create something like

-- The typing doesn't really work for this
extractMaybe :: forall a b c d. (Tuple a b) -> Maybe (Tuple c d)
extractMaybe (Tuple (Just a) (Just b)) = Just $ Tuple a b
extractMaybe (Tuple (Just a) b) = Just $ Tuple a b
extractMaybe (Tuple a (Just b)) = Just $ Tuple a b
extractMaybe (Tuple Nothing _) = Nothing
extractMaybe (Tuple _ Nothing) = Nothing
extractMaybe (Tuple a b) = Just $ Tuple a b

While reading about applicatives and bind, this is the sort of refactoring I would see a lot. I don't really have the experience yet to apply those lessons for myself.

If I have a structure that may contain an ADT (An Array of Eithers, or Maybes, or Arrays) and I want to define a way pulling those "out," is there a generalized way to define that?


Purescript by Example has an example with this type signature for a function:

combineList :: forall f a. Applicative f => List (f a) -> f (List a)

which is doing the same thing, only for a List. N-Tuples complicate this because every element can have a different type. If I call extractMaybe on a Tuple, I now a have a Tuple with any types except for Maybe (or I have Nothing).

I could write the same thing for Either and if I use both, I could know I have a Tuple with any types except for Maybe and Either (or I have Nothing).

I'm not even entirely sure this is the most helpful direction to go in. It seems helpful in situations like mapMaybe where you want to filter out all Nothings and Lefts while unwrapping those types as well.

2
It's not really clear what your overall question is, so it's hard to answer! But you're right that your extractFstMaybe can be written more abstractly/cleanly. The section of Purescript by Example that you link to is about Traversable, and since Maybe is Applicative and Tuple a is Traversable, sequence can be specialised to the type forall a b. Tuple a (Maybe b) -> Maybe (Tuple a b). That's not quite what you want, because it "extracts" the wrong element of the tuple - but (TBC)... - Robin Zigmond
(continued from above): swap comes to your rescue - so you can write extractFstMaybe = map swap . sequence . swap - Robin Zigmond
Thanks. Took me a bit, but this does make sense. I wound up re-writing it as swap >>> sequence >>> map swap and that somehow clicked - Mrk Sef

2 Answers

2
votes

This is exactly ltraverse:

module Main where

import Data.Bitraversable (ltraverse)
import Data.Int (fromString)
import Data.Tuple (Tuple)
import Data.Maybe (Maybe)

keyToInt :: forall a. Tuple String a -> Maybe (Tuple Int a)
keyToInt = ltraverse fromString 

Whenever you're destructuring a type and constructing it back within another type (e.g. Maybe) that's when you want to traverse. In this case, Tuple has two things you can traverse over, making it Bitraversable. For this function, you want to traverse over the left hand side of the Tuple - i.e. ltraverse.

1
votes

I might program this as:

keyToInt (Tuple a b) = ado
  a' <- fromString a
  in Tuple a' b

because it is close to many similar needs. You can easily apply something to b, or pattern match on something other than Tuple, or combine the result with something other than Tuple.