compiler icon indicating copy to clipboard operation
compiler copied to clipboard

Matching on negative literals results in a parser error

Open JDemler opened this issue 5 years ago • 19 comments

in elm 0.19:

isMinusOne : Int -> Bool
isMinusOne val =
   case val of
       -1 ->
           True
                     
       _ ->
           False

Results in:

parse error
Line 4, Column 8
Something went wrong while parsing a `case` expression in isMinusOne's
definition.

3|    case val of
4|        -1 ->
          ^
I was expecting to see a pattern, like `name` or (Just x)

If parentheses are added, which were needed in elm 0.18:

parse error
Line 4, Column 9
Something went wrong while parsing an expression (in parentheses) in
isMinusOne's definition.

4|        (-1) ->
           ^
I was expecting:

  - a pattern, like `name` or (Just x)
  - a closing paren

Link to ellie example

JDemler avatar Aug 30 '18 21:08 JDemler

I think it's quite an important issue as we spent about 30% of our migration time from Elm 18 to Elm 19 fixing negative number pattern matches in our code and I see no practical reason why it's not working.

Current bypasses are also looking quite ugly:

case i of
   1 ->
        ...
   2 ->
        ...
   i ->
       if i == -1 then
           ...
       else if i == -2 then
          ...
       else
          ...

or

case i of
   1 ->
        ...
   2 ->
        ...
   i ->
      case negate i of
          1 ->
              ...
          2 ->
               ...
          _ ->
              ...

Voronchuk avatar Sep 03 '18 06:09 Voronchuk

We use

 case (num + 1) of
        0 ->

        1 ->

        2 ->

Which is not ugly but very WAT?-like and all entries need to be changed... Nevertheless it somehow amuses me.

JDemler avatar Sep 03 '18 08:09 JDemler

Why are you doing this? Instead of fake examples, can you explain how this comes up?

I'm imagining that an enum like type Mode = Print | Pdf | Html is getting represented as integers for unclear reasons. What is the real situation?

evancz avatar Sep 04 '18 18:09 evancz

@evancz at very least we use such matches for decoding (converted to strings for now):

{-| Convert string value to StageRank
-}
rankFromValue : String -> StageRank
rankFromValue rank =
    case rank of
        "-2" ->
            StageRankBad

        "-1" ->
            StageRankFlawed

        "0" ->
            StageRankNormal

        "1" ->
            StageRankMinor

        "2" ->
            StageRankMajor

        "3" ->
            StageRankGreat

        _ ->
            StageRankInvalid

Voronchuk avatar Sep 04 '18 19:09 Voronchuk

@evancz in my app, I am testing the difference between two numbers, which indicate the positions of two values in a list. If the difference is exactly one or exactly negative one (indicating left-hand or right-hand adjacency), then the app should do something special (different in each case), and if the difference is any other value then the app should do nothing. So the code looked like:

case from - to of
    (-1) -> -- left-hand adjacent
    1 -> -- right hand adjacent
    _ -> -- do nothing

I've replaced this with

let
    difference = from - to
in
    if difference == -1
    then -- left-hand adjacent
    else
        if difference == 1
        then -- r.h. adjacent
        else -- do nothing

Which works, but at the cost of clarity and concision.

aecay avatar Sep 07 '18 16:09 aecay

@evancz In my app, I'm letting users upvote (+1), downvote (-1), and undo a vote (0) on comments like on Reddit, and I do have a type like so...

type VoteDirection
    = None
    | Up
    | Down

but the use case here is trying to decode it from JSON integers.

ghost avatar Sep 24 '18 23:09 ghost

To add another real world example, the button property of a PointerEvent represents having no buttons pressed with a -1. I encountered this while making a decoder as follows:

type Button
    = NoButton
    | MainButton
    -- ...

buttonDecoder : JD.Decoder Button
buttonDecoder =
    let
        buttonFromInt int =
            case int of
                (-1) ->
                    NoButton

                0 ->
                    MainButton

                -- ...

    in
    JD.map buttonFromInt JD.int

capicue avatar Oct 01 '18 18:10 capicue

@evancz In our McMaster outreach program Graphics library, we give the students an (x,y) vector as a tuple representing arrow keys where, for example, (-1,0) means the left arrow key is pressed. It's convenient to have this be able to be matched by a case expression in their code. We were surprised that all the sudden our game code didn't compile in 0.19 so this is definitely an annoyance.

CSchank avatar Oct 05 '18 22:10 CSchank

case x of c would be preferred as a language feature, case msg of Msg just the byproduct.

fansgit avatar Oct 07 '18 21:10 fansgit

@evancz I have a real world example too. This is my code that used to work with elm 0.18:

        newLocationsWithCastling =
          case pieceLocation.piece of
            Piece.WhiteKing ->
              case destSquare - departSquare of
                (-2) ->
                  Dict.insert (BB.toString BB.WhiteRooks)
                    { bb =
                      BB.or (BB.and whiteRooksBb (BB.complement (BB.fromSquares [ 0 ]))) (BB.shiftRight departLocBb 1)
                    , piece = Piece.WhiteRook
                    }
                    newLocations

                2 ->
                  Dict.insert (BB.toString BB.WhiteRooks)
                    { bb =
                      BB.or (BB.and whiteRooksBb (BB.complement (BB.fromSquares [ 7 ]))) (BB.shiftLeft departLocBb 1)
                    , piece = Piece.WhiteRook
                    }
                    newLocations

                _ ->
                  newLocations

            Piece.BlackKing ->
              case destSquare - departSquare of
                (-2) ->
                  Dict.insert (BB.toString BB.BlackRooks)
                    { bb =
                      BB.or (BB.and blackRooksBb (BB.complement (BB.fromSquares [ 56 ]))) (BB.shiftRight departLocBb 1)
                    , piece = Piece.BlackRook
                    }
                    newLocations

                2 ->
                  Dict.insert (BB.toString BB.BlackRooks)
                    { bb =
                      BB.or (BB.and blackRooksBb (BB.complement (BB.fromSquares [ 63 ]))) (BB.shiftLeft departLocBb 1)
                    , piece = Piece.BlackRook
                    }
                    newLocations

                _ ->
                  newLocations

            _ ->
              newLocations

where I have to detect in chess whether the King moved 2 squares on the left or 2 on the right (castling) and to move the Rook accordingly. Of course I can use a work around but common sense and mathematical convention orders that when you are at a point which you define as the origin assigning it a value of 0 then moving e.g. 2 meters on the left is -2 from you current location and moving 2 meters on the right is +2 or 2. Why should we consider that negative numbers are of any difference of positive ones and treat them differently? I cannot see any practical reason. In my opinion it's a pity for elm not to support negative numbers in pattern matching

vjorjo avatar Oct 10 '18 10:10 vjorjo

@Voronchuk hi, you can save a nesting level by matching on a pair of (i, negate i), e.g.

test : Int -> Int
test i = case (i, negate i) of
  (_, 2) -> 0
  (_, 1) -> 0
  (0, _) -> 1
  (1, _) -> 1
  _ -> 2

yawaramin avatar Jan 06 '19 01:01 yawaramin

@yawaramin Good point. Still seems unnecessarily complicated in my opinion (and probably slightly slower)!

CSchank avatar Jan 07 '19 02:01 CSchank

I've bumped into this as well, trying to decode a JSON payload where -1 is used as a flag.

But practical examples aside, it just seems odd in principle that the language will allow you to pattern match against integers, unless those integers happen to be negative.

wjdhamilton avatar Aug 07 '19 08:08 wjdhamilton

We've run into this speed bump in our 0.18 → 0.19 porting efforts as well. Here's our real-world example:

ratingValueToCssClass ratingValue =
    case ratingValue of
        (-1) ->
            .unrated

        0 ->
            .field0

        1 ->
            .field1

        2 ->
            .field2

        3 ->
            .field3

        4 ->
            .field4

        5 ->
            .field5

        _ ->
            .invalidRating

This could certainly be replaced by decoding our JSON API to a richer type than an integer, but we'd probably just end up with the same issue in our JSON decoder.

sentience avatar Sep 19 '19 05:09 sentience

I use a modular scale for typography, and I need to match certain integers in this scale, which can be above or below 0.

I understand the desire to help developers avoid bad patterns by using custom types, but it appears quite arbitrary to assume that matching on negative numbers is undesirable, or even that an ideal program should not do this. At best one could assume it shouldn't happen too often.

If the principle is to keep it simple and encourage good patterns, it appears to me that a syntax error is inappropriate, as it is not at all obvious why should one avoid it.

tgelu avatar Nov 23 '20 21:11 tgelu

this hasn't been fixed?

domino14 avatar Jan 25 '22 02:01 domino14

If I may add to this issue...

MIDI file format saves key signatures as two values. One representing the number of "sharps", and a single bit value which is set to 1 if the key is a minor key (that I just decode as a boolean). A negative value for "sharps" is used when the key contains flats instead of sharps.

makeSignature : Int -> Bool -> Maybe Signature
makeSignature sharps isMinor =
    let
        majorOrMinor a b =  
            if isMinor then
                Just { key = b, majorOrMinor = Minor }

            else
                Just { key = a, majorOrMinor = Major }
    in
    case sharps of
        7 ->
            majorOrMinor CSharp ASharp 
        6 ->
            majorOrMinor FSharp DSharp 

        5 ->
            majorOrMinor B GSharp 

        4 ->
            majorOrMinor E CSharp 

        3 ->
            majorOrMinor A FSharp 

        2 ->
            majorOrMinor D B

        1 ->
            majorOrMinor G E 

        0 ->
            majorOrMinor C A 

        -1 ->
            majorOrMinor F D 

        -2 ->
            majorOrMinor BFlat G 

        -3 ->
            majorOrMinor EFlat C 

        -4 ->
            majorOrMinor AFlat F 

        -5 ->
            majorOrMinor DFlat BFlat 

        -6 ->
            majorOrMinor GFlat EFlat 

        -7 ->
            majorOrMinor CFlat GSharp 

        _ ->
            Nothing

I'll most likely just get the absolute value when the key contains flats to work around this limitation, but I feel like, since a negative integer is a valid value for an Int, it should be a value that can be matched on.

Flecks1 avatar Apr 24 '22 22:04 Flecks1

hey @yawaramin, I'm curious how you're finding OCaml-To-JS relative to Elm 👀

mrmizz avatar Jan 28 '24 22:01 mrmizz

@mrmizz that's off-topic here but feel free to email me

yawaramin avatar Jan 28 '24 22:01 yawaramin