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

TL; DR:
How to write a function that is polymorphic in the return type? I am working on an exercise where the task is to write a function that is able to parse a String and, depending on its contents, generate either Vector [Int] , Vector [Char] , or Vector [String] .

Longer version:
Here are some examples of how the intended function will behave:

  • The string "1 2\n3 4" will generate Vector [Int] , which consists of two lists: [1,2] and [3,4] .

  • The string "'t' 'i' 'c'\n't' 'a' 'c'\n't' 'o' 'e'" will generate a Vector [Char] (that is, composed of "tic" , "tac" and "toe" lists).

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

Error checking / exception handling is not a problem for this exercise. At this stage, all tests are performed cleanly, i.e. This is not in the kingdom of the IO monad

What I still have:

I have a function (and a new data type) that is able to classify a string. I also have functions (one for each Int , Char and String ) that can convert a string to the desired Vector.

My question is: how to combine these three conversion functions into one function?

What I tried:

  • (Obviously, this is not typecheck if I use three transformations of a function into one function (i.e., using the case..of structure to match the pattern in the VectorType string of the string.

  • I tried to create a Vectorable class and define a separate instance for each type; I quickly realized that this approach only works if the arguments to the functions differ in type. In our case, the type of the argument does not change (i.e. Always a String ).

My code is:

Some comments

  • Analysis: the mySplitter object and the mySplit function handle parsing. This is admittedly a crude parser based on the Splitter and split from Data.List.Split.Internals .

  • Classification: The classify function is able to determine the final VectorType based on a string.

  • Conversion: the functions toVectorNumber , toVectorChar and toVectorString can convert a string to the types Vector [Int] , Vector [Char] and Vector [String] respectively.

  • As a side note, I tried CorePrelude based on a recommendation from a mentor. This is why you will see that I am using generic versions of the usual Prelude functions.

The 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 
+8
string polymorphism parsing haskell
source share
2 answers

You can not.

Covariant polymorphism is not supported in Haskell and would not be useful if it were.


This is basically all that needs to be answered. Now why is this so.

You should not "return a polymorphic value", as, for example, OO languages, because the only reason for returning any value is the use of it in other functions. Now in OO languages ​​you have no functions except the methods that come with the object, so it’s pretty easy to “return different types”: each of them will have its own suitable methods, and they can vary. (Is this a good idea another question.)

But in Haskell, functions come from other sources. They are not aware of implementation changes for a particular instance, so the only way that such functions can be safely identified is to know all the possible implementations. But if your return type is really polymorphic, this is impossible, because polymorphism is an "open" concept (it allows you to add new implementations at any time later).

Instead, Haskell has a very convenient and absolutely safe mechanism for describing a closed set of "instances" - you already used it yourself! ATD.

 data PolyVector = NumbersVector (Vector [Int]) | CharsVector (Vector [Char]) | StringsVector (Vector [String]) 

This type of refund you want. The function will not be polymorphic as such, it will simply return a more universal type.


If you insist that it should be polymorphic

Now ... in fact, Haskell has a way to deal with "polymorphic returns". As in OO, when you declare that you are returning a subclass of the specified class. Well, you cannot “return a class” at all in Haskell, you can only return types. But this can be done to express "any case ...". He called existential quantification.

 {-# LANGUAGE GADTs #-} data PolyVector' where PolyVector :: YourVElemClass e => Vector [e] -> PolyVector' class YourVElemClass where ...? instance YourVElemClass Int instance YourVElemClass Char instance YourVElemClass String 

I don't know if this looks intriguing to you. True, it is much harder and harder to use; you can simply not use only any possible results, but you can only use elements using YourVElemClass methods. GADTs can be extremely useful in some applications, but they usually include classes with very deep mathematical motivation. YourVElemClass doesn't seem to have that kind of motivation, so you would be much better off with a simple alternative to ADT than existential quantification.

There is the famous ranted with existential eyes of Luc Palmer (note that he uses a different existential syntax, which I consider obsolete, since GADT is strictly more general).

+16
source share

Just use the amount 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 
+8
source share

All Articles