Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to return a polymorphic type in Haskell based on the results of string parsing?

TL;DR:
How can I write a function which is polymorphic in its return type? I'm working on an exercise where the task is to write a function which is capable of analyzing a String and, depending on its contents, generate either a Vector [Int], Vector [Char] or Vector [String].

Longer version:
Here are a few examples of how the intended function would behave:

  • The string "1 2\n3 4" would generate a Vector [Int] that's made up of two lists: [1,2] and [3,4].

  • The string "'t' 'i' 'c'\n't' 'a' 'c'\n't' 'o' 'e'" would generate a Vector [Char] (i.e., made up of the lists "tic", "tac" and "toe").

  • The string "\"hello\" \"world\"\n\"monad\" \"party\"" would generate a Vector [String] (i.e., ["hello","world"] and ["monad","party"]).

Error-checking/exception handling is not a concern for this particular exercise. At this stage, all testing is done purely, i.e., this isn't in the realm of the IO monad.

What I have so far:

I have a function (and new datatype) which is capable of classifying a string. I also have functions (one for each Int, Char and String) which can convert the string into the necessary Vector.

My question: how can I combine these three conversion functions into a single function?

What I've tried:

  • (It obviously doesn't typecheck if I stuff the three conversion functions into a single function (i.e., using a case..of structure to pattern match on VectorType of the string.

  • I tried making a Vectorable class and defining a separate instance for each type; I quickly realized that this approach only works if the functions' arguments vary by type. In our case, the the type of the argument doesn't vary (i.e., it's always a String).

My code:

A few comments

  • Parsing: the mySplitter object and the mySplit function handle the parsing. It's admittedly a crude parser based on the Splitter type and the split function from Data.List.Split.Internals.

  • Classifying: The classify function is capable of determining the final VectorType based on the string.

  • Converting: The toVectorNumber, toVectorChar and toVectorString functions are able to convert a string to type Vector [Int], Vector [Char] and Vector [String], respectively.

  • As a side note, I'm trying out CorePrelude based on a recommendation from a mentor. That's why you'll see me use the generalized versions of the normal Prelude functions.

Code:

import qualified Prelude
import CorePrelude                   

import Data.Foldable (concat, elem, any)
import Control.Monad (mfilter)
import Text.Read (read)
import Data.Char (isAlpha, isSpace)

import Data.List.Split (split)
import Data.List.Split.Internals (Splitter(..), DelimPolicy(..), CondensePolicy(..), EndPolicy(..), Delimiter(..))

import Data.Vector ()                       
import qualified Data.Vector as V           

data VectorType = Number | Character | TextString deriving (Show)

mySplitter :: [Char] -> Splitter Char
mySplitter elts = Splitter { delimiter        = Delimiter [(`elem` elts)]
                           , delimPolicy      = Drop
                           , condensePolicy   = Condense
                           , initBlankPolicy  = DropBlank
                           , finalBlankPolicy = DropBlank }

mySplit :: [Char]-> [Char]-> [[Char]]
mySplit delims = split (mySplitter delims)           

classify :: String -> VectorType
classify xs
  | '\"' `elem` cs = TextString
  | hasAlpha cs = Character
  | otherwise = Number
  where
    cs = concat $ split (mySplitter "\n") xs
    hasAlpha = any isAlpha . mfilter (/=' ')

toRows :: [Char] -> [[Char]]
toRows = mySplit "\n"

toVectorChar ::    [Char] -> Vector [Char]
toVectorChar =   let toChar = concat . mySplit " \'" 
                 in V.fromList . fmap (toChar) . toRows

toVectorNumber  :: [Char] -> Vector [Int]
toVectorNumber = let toNumber = fmap (\x -> read x :: Int) . mySplit " "
                 in  V.fromList . fmap toNumber . toRows

toVectorString  :: [Char] -> Vector [[Char]]
toVectorString = let toString = mfilter (/= " ") . mySplit "\""
                 in  V.fromList . fmap toString . toRows
like image 972
iceman Avatar asked Nov 14 '14 23:11

iceman


Video Answer


1 Answers

Easy, use an sum type!

data ParsedVector = NumberVector (Vector [Int]) | CharacterVector (Vector [Char]) | TextString (Vector [String]) deriving (Show)

parse :: [Char] -> ParsedVector
parse cs = case classify cs of
  Number     -> NumberVector $ toVectorNumber cs
  Character  -> CharacterVector $ toVectorChar cs
  TextString -> TextStringVector $ toVectorString cs
like image 103
rampion Avatar answered Oct 07 '22 20:10

rampion