-
Notifications
You must be signed in to change notification settings - Fork 108
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Sumtype enums #151
Sumtype enums #151
Conversation
Thanks for your pull request. It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). 📝 Please visit https://cla.developers.google.com/ to sign. Once you've signed, please reply here (e.g.
|
I signed the CLA (by adding myself to the takt OSS google group), cc @judah |
CLAs look good, thanks! |
-- = Prelude.show k | ||
-- showEnum k = Prelude.show k | ||
-- readEnum "Enum2a" = Prelude.Just Enum2a -- alias | ||
-- readEnum k = Text.Read.readMaybe k |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suspect this doesn't do what you think it does, for two reasons:
- If the value is known, we should use the constructor instead of
FooEnum'Unrecognized
- If the value is not known, I think this reads a value of type
FooEnum
rather than of typeint
.
You probably want instead something like readMaybe k >>= maybeToEnum
Also please add tests for the above cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
readMaybe k >>= maybeToEnum
assumes that the textual representation of these enum's are Ints. I was trying to reproduce the pre-existing logic in which strings like "Enum2a", "Enum2", etc are converted into instances of the sum type. Should this function instead take strings holding ints mapping to different enum values, eg readEnum "1" -> Just Foo'Enum1
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I see, I think I misread your code earlier.
Taking another look, we actually can't use the Show
and Read
instances for FooEnum
to implement showEnum/readEnum
. Because for nested enums like
message Bar {
enum Foo { A = 1; }
}
we want showEnum Bar'A = "A"
and readEnum "A" = Bar'A
, not showEnum Bar'A = "Bar'A"
and readEnum "Bar'A" = Bar'A
. It could probably be described better in the docs, but readEnum/showEnum
are specifically for the proto text format representation of the enum:
https://github.com/google/proto-lens/blob/master/proto-lens/src/Data/ProtoLens/Message.hs#L231
I think we should just take the string representations (not the ints like I suggested earlier), as in the code for proto2:
https://github.com/google/proto-lens/blob/master/proto-lens-protoc/src/Data/ProtoLens/Compiler/Generate.hs#L374
readEnum "Enum2" = Enum2
readEnum "Enum2a" = Enum2a
etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That sounds good, but that still leaves us with no ability to serialize/deserialize unrecognized enum's. I'm assuming readEnum . showEnum == id
is a property we want to maintain. If so what should showEnum Foo'FooEnum Foo'Unrecognized...
result in?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, good point, we do need something sensible for showEnum
at least. How about this?
showEnum Enum2 = "Enum2"
showEnum (Foo'Unrecognized (Foo'UnrecognizedValue n)) = show n
readEnum "Enum2" = Enum2
readEnum "Enum2a" = Enum2
readEnum s = Foo'Unrecognized . UnrecognizedValue <$> readMaybe s
Since in proto text format, it's fine to refer to an enum by its numeric value rather than the case name.
constructorNumbers = map (second (fromIntegral . (^. number))) constructors | ||
|
||
succPairs = zip constructorNames $ tail constructorNames | ||
succDecl funName boundName thePairs = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think succ
should be equivalent to toEnum . incr . fromEnum
. The implications are kind of subtle though compared to the proto2 semantics listed here: https://github.com/google/proto-lens/blob/master/proto-lens/src/Data/ProtoLens/Message/Enum.hs#L2
If we do make that change, we should note in Data.ProtoLens.Message.Enum
that it only applies to proto2.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good. Is not erroring on succ maxEnum
and instead returning SomeEnum'UnrecognizedValue... (fromEnum maxEnum + 1
) the desired behavior here?
Do you also want me to redefine pred
as toEnum . (-1) . fromEnum
? Or should pred minEnum
still result in an error?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this would break things, eg for FooEnum
as defined in proto3.proto:
enum FooEnum {
option allow_alias = true;
Enum1 = 0;
Enum2 = 3;
Enum2a = 3;
}
it seems like we want succ Enum1 == Enum2
, not succ Enum1 == Foo'FooEnum'Unrecognized (Foo'FooEnum'UnrecognizedValue 2)
proto-lens-tests/tests/proto3.proto
Outdated
@@ -16,17 +16,18 @@ message Foo { | |||
} | |||
Sub sub = 5; | |||
|
|||
enum FooEnum { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What was the reason for changing this test? My preference is to add a new test case if we want to support the different situation. (Or, at minimum, add another test that does check naming of nested enums.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that was due to a transient issue, I've reverted this change.
@@ ("Prelude.shows" @@ "k")) | ||
] | ||
] | ||
-- data FooEnum = Enum1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Formatting nit in this comment:
-- data FooEnum
-- = Enum1
-- | Enum2
-- | FooEnum'Unrecognized ...
|
||
unrecognizedValueType = tyCon $ unQual unrecognizedValueName | ||
|
||
constructors :: [(Name, EnumValueDescriptorProto)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a shame this now duplicates logic from the proto2 code...not sure if there's a good way to re-combine the two. (They were split apart more in #137.)
@@ -359,3 +359,7 @@ modifyModuleName :: (String -> String) -> ModuleName -> ModuleName | |||
modifyModuleName f (Syntax.ModuleName _ unpacked) = | |||
Syntax.ModuleName () $ f unpacked | |||
|
|||
|
|||
modifyIdent :: (String -> String) -> Name -> Name |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code structure nit: we try to define names inside the Definitions module and use them in Generate. In this case, adding new fields to EnumInfo
with the Name
s for the unrecognized types:
https://github.com/google/proto-lens/blob/master/proto-lens-protoc/src/Data/ProtoLens/Compiler/Definitions.hs#L157
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall looks good; just a few nits remaining.
, [ match "readEnum" [stringPat pn] | ||
$ "Prelude.Just" @@ con (unQual n) | ||
| v <- enumValues info | ||
, let n = enumValueName v | ||
, let pn = T.unpack $ enumValueDescriptor v ^. name | ||
] ++ | ||
[match "readEnum" [pVar "k"] $ ("Prelude.>>=" @@ paren ("Text.Read.readMaybe" @@ "k")) @@ ("Data.ProtoLens.maybeToEnum")] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Break this line which has gotten pretty long.
@@ -68,6 +68,7 @@ import Text.PrettyPrint | |||
testMain :: [Test] -> IO () | |||
testMain = defaultMain | |||
|
|||
-- todo use this for enum msg on Foo |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove comment
, testGroup "enum" | ||
[ testCase "aliases are exported" $ Foo'Enum2 @=? Foo'Enum2a | ||
, serializeTo "serializeTo enum" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add a copy of this test which uses an unrecognized value?
Also: replace l.107 with more explicit generation:
tagged 6 $ VarInt 3
(where 6
is the proto tag of that field, and 3
is the value corresponding to Enum2
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ping on the first line of the above comment (unless I'm misreading the diff).
More specifically, please add this test which checks that unrecognized values round-trip through serialization as expected.
serializeTo "serializeTo enum unknown"
(def & enum .~ toEnum 5 :: Foo)
"enum: 5"
$ tagged 6 $ VarInt 5
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
whoops, sorry about that. Fixed.
@=? map show [Foo'Enum1, Foo'Enum2, Foo'Enum2a, toEnum 5] | ||
["Enum1", "Enum2", "Enum2", "6"] | ||
@=? map showEnum [Foo'Enum1, Foo'Enum2, Foo'Enum2a, toEnum 6] | ||
[Just Foo'Enum1, Just Foo'Enum2, Just Foo'Enum2, Nothing, Nothing] | ||
[Just Foo'Enum1, Just Foo'Enum2, Just Foo'Enum2, maybeToEnum 4, maybeToEnum 5] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test nit (working around the fact that the constructor isn't provided): Add a new test that checks fromEnum <$> maybeToEnum 4 == Just 4
-- (FooEnum'UnrecognizedValue (Prelude.fromIntegral k))) | ||
-- showEnum (FooEnum'Unrecognized (FooEnum'UnrecognizedValue k)) | ||
-- = Prelude.show k | ||
-- showEnum k = Prelude.show k |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this comment needs to be updated since you're not calling Prelude.show
anymore.
-- deriving (Prelude.Eq, Prelude.Ord, Prelude.Show, Prelude.Read) | ||
, newtypeDecl unrecognizedValueName | ||
"Data.Int.Int32" | ||
$ deriving' ["Prelude.Eq", "Prelude.Ord", "Prelude.Show, Prelude.Read"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we remove the Read
instance, to make sure it's not being used accidentally?
(Also because we'd like toEnum/maybeToEnum
to be the only way to construct these values)
Up to you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks again for taking on this change!
One small request remaining, otherwise LGTM.
, testGroup "enum" | ||
[ testCase "aliases are exported" $ Foo'Enum2 @=? Foo'Enum2a | ||
, serializeTo "serializeTo enum" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ping on the first line of the above comment (unless I'm misreading the diff).
More specifically, please add this test which checks that unrecognized values round-trip through serialization as expected.
serializeTo "serializeTo enum unknown"
(def & enum .~ toEnum 5 :: Foo)
"enum: 5"
$ tagged 6 $ VarInt 5
#150
This change is![Reviewable](https://camo.githubusercontent.com/23b05f5fb48215c989e92cc44cf6512512d083132bd3daf689867c8d9d386888/68747470733a2f2f72657669657761626c652e696f2f7265766965775f627574746f6e2e737667)