3
votes

I recently started learning Haskell and I'm trying to rewrite something I did for an interview in python in Haskell. I'm trying to convert a string from camel case to underscore separated ("myVariableName" -> "my_variable_name"), and also throw an error if the first character is upper case.

Here's what I have:

import qualified Data.Char as Char

translate_java :: String -> String
translate_java xs = translate_helper $ enumerate xs
    where 
        translate_helper [] = []
        translate_helper ((a, num):xs)
            | num == 1 and Char.isUpper a = error "cannot start with upper"
            | Char.isUpper a              = '_' : Char.toLower a : translate_helper xs
            | otherwise                   = a : translate_helper xs


enumerate :: (Num b, Enum b) => [a] -> [(a,b)]
enumerate xs = zip xs [1..]

I realize It's pretty likely I'm going about this in a weird way, and I'd love advice about better ways to implement this, but I'd like to get this to compile as well. Here's the error I'm getting now:

Prelude> :r
[1 of 1] Compiling Main             ( translate.hs, interpreted )

translate.hs:4:20:
    No instance for (Num
                       (([Bool] -> Bool) -> (Char -> Bool) -> Char -> t))
      arising from a use of `translate_helper' at translate.hs:4:20-35
    Possible fix:
      add an instance declaration for
      (Num (([Bool] -> Bool) -> (Char -> Bool) -> Char -> t))
    In the first argument of `($)', namely `translate_helper'
    In the expression: translate_helper $ enumerate xs
    In the definition of `translate_java':
        translate_java xs
                         = translate_helper $ enumerate xs
                         where
                             translate_helper [] = []
                             translate_helper ((a, num) : xs)
                                                | num == 1 and Char.isUpper a
                                                = error "cannot start with upper
"
                                                | Char.isUpper a
                                                = '_' : Char.toLower a : transla
te_helper xs
                                                | otherwise = a : translate_help
er xs
Failed, modules loaded: none.

Any explanation of what's going on here would be great. I really don't understand where "(Num (([Bool] -> Bool) -> (Char -> Bool) -> Char -> t))" is coming from. I'd think the type declaration for translate_helper would be something like [(a,b)] -> [a]?

4

4 Answers

5
votes

You have to replace and by &&. The first one is a function (prefix) that receives a list of boolean values and calculates an and of them all. The second one is a true logical and. The error message is a little bit confusing though. Whenever I get such a strange error message, I usually start to annotate my code with type signatures. Then the compiler is able to give you a more detailed description of what went wrong.

5
votes

Others have mentioned that you should use (&&) instead of and, so I'll answer your other question: no, I don't think you're going about this in a weird way.

But... I do think it can be even more elegant!

translate_java (x:xs) | isUpper x = error "cannot start with an upper"
translate_java xs = concatMap translate xs where
    translate x = ['_' | isUpper x] ++ [toLower x]

There's a few interesting things going on here:

  1. The special case is checked straight away. Don't wait until you're recursing to do this!
  2. The concatMap function is really handy in a lot of cases. It's just a map followed by a concat. If I were writing this myself, I'd probably use xs >>= translate instead.
  3. That ['_' | isUpper x] is a list comprehension; this is a cute idiom for making a list with either 0 or 1 elements in it, depending on whether a predicate holds.

Other than that, the code should be fairly self-explanatory.

4
votes

The problem is this:

| num == 1 and Char.isUpper a = ...

and is not an infix operator; rather it is a function:

and :: [Bool] -> Bool

So it is interpreting 1 and Char.isUpper a as applying three arguments to the "function" 1. Use && instead.

The error message comes from the way numerals are interpreted. A numeral, say, 1 is actually polymorphic; the specific type it gets depends on the type that is needed. That's why you can say x+1 and it will work whether x is an integer or a double or whatever. So the compiler inferred that the type of 1 needs to be a three-argument function, and then tried to find a numeric type matching that so it could convert 1 into that type (and, naturally, failed).

3
votes

Here's my solution. It's not as masterful as the answer Daniel Wagner gave using concatMap and the list comprehension, but it's perhaps easier to understand for the beginner.

conv :: String -> String
conv [] = []
conv s@(x:xs) = if Char.isUpper x 
                then error "First character cannot be uppercase"
                else change s

change :: String -> String
change [] = []
change (x:xs) = if Char.isUpper x 
                then '_' : Char.toLower x : change xs 
                else x : change xs

The function conv really just checks your criterion that the first character must not be uppercase, and if it isn't it hands over the string to the function change, which does the work. It goes through all the characters one by one, building a list, and if the character is uppercase, it adds an underscore followed by the lowercase version of the character, otherwise if the character is already lowercase it just adds it as it is.