Skip to content

Question on using ToRecord #2

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

Open
bbarker opened this issue Sep 5, 2019 · 13 comments
Open

Question on using ToRecord #2

bbarker opened this issue Sep 5, 2019 · 13 comments

Comments

@bbarker
Copy link
Contributor

bbarker commented Sep 5, 2019

In the following example, I get an instance not found error for ToRecordOption:

type MyRecRows  = (foo:: Int, bar:: String)
type MyRec = Record MyRecRows

recTest :: Opt.Option MyRecRows -> String
recTest myRecOpt = (show fooInt)
  <> "\n"
  <> (show $ Opt.get(SProxy :: _ "bar") myRecOpt)
  where
    fooInt :: Int
    fooInt = ((Opt.toRecord' myRecOpt) :: MyRec).foo

One point of confusion is that I think this should be returning a "maybe-fied" record, so I should really have fooInt :: Maybe Int:

recTest :: Opt.Option MyRecRows -> String
recTest myRecOpt = (show fooInt)
  <> "\n"
  <> (show $ Opt.get(SProxy :: _ "bar") myRecOpt)
  where
    fooInt :: Maybe Int
    fooInt = (Opt.toRecord' myRecOpt).foo

But the error becomes worse:

    Option.ToRecordOption t3
                          ( bar :: String
                          , foo :: Int
                          )
                          ( foo :: Maybe Int
                          | t0
                          )

Is there a suggested way around this problem?

Also, I wonder if it might be possible to have another function, toRecordMay or some such, that goes from Option opt -> Maybe rec, such that we get a populated record only if every field of the record (or, perhaps easier to reason about: every field in the option) is not Nothing*, otherwise just return Nothing. I think this could be highly useful for accumulating state incrementally.

*One exception is that if some fields of the output record are themselves Maybe values, but really we would still be requiring a value is Just x where x might be Nothing. Maybe this is worthy of its own issue to discuss separately.

@joneshf
Copy link
Owner

joneshf commented Sep 5, 2019

Is there a suggested way around this problem?

I think this is an inference issue. You're right that you'll end up getting a { foo :: Maybe Int, bar :: Maybe String }, but the instance doesn't seem to know that. If you type your second attempt explicitly, it should work:

type MyRec = Record (foo :: Maybe Int, bar :: Maybe String)

recTest :: Opt.Option MyRecRows -> String
recTest myRecOpt = (show fooInt)
  <> "\n"
  <> (show $ Opt.get(SProxy :: _ "bar") myRecOpt)
  where
    fooInt :: Maybe Int
    fooInt = ((Opt.toRecord' myRecOpt) :: MyRec).foo

I'm not entirely sure if this is an issue with this package or something in the type checker. I'll see if the fundeps can be altered to address this.

Also, I wonder if it might be possible to have another function, toRecordMay or some such, that goes from Option opt -> Maybe rec, such that we get a populated record only if every field of the record (or, perhaps easier to reason about: every field in the option) is not Nothing*, otherwise just return Nothing.

Sure, I'll try to throw something together. Kind of reminds me of Data.Traversable.sequence.

@bbarker
Copy link
Contributor Author

bbarker commented Sep 5, 2019

Great! wasn't sure if it was possible. And yes, the explicit annotation fixes it. I guess this brings up another question: is there any known way to auto-derive MyRecRowsMay from MyRecRows, in the below? I don't think so, as I haven't been able to find any means of mapping over RowList at the type level.

type MyRecRows  = (foo:: Int, bar:: String)
type MyRecRowsMay  = (foo:: Maybe Int, bar:: Maybe String)

type MyRec = Record MyRecRows
type MyRecMay = Record MyRecRowsMay

But, not a big deal, I can always use the slightly more verbose fromRecord syntax.

Finally, a last question: in order to facilitate easier use of set, would it be possible to have something like Option.keyedEmpty, that creates the option with all keys, and associated values set to Nothing?
Again, not sure how this would be achieved - the problem isn't quite as bad as the above, I think, since we only need to map over SProxies at the value level, but still not sure how to go about it. The alternative seems to be to start with Option.empty and, for each record type, create a function that manually inserts Nothing.

I wonder if purescript-heterogenous could be useful for any of this, but it is way outside my comfort level with types.

@joneshf
Copy link
Owner

joneshf commented Sep 6, 2019

is there any known way to auto-derive MyRecRowsMay from MyRecRows, in the below?

The pattern is to Add a type parameter!:

type MyRecRows (a :: Type -> Type)
  = ( foo :: a Int
    , bar :: a String
    )

type Identity a
  = a

type MyRec
  = Record (MyRecRows Identity)

type MyRecMay
  = Record (MyRecRows Maybe)

in order to facilitate easier use of set, would it be possible to have something like Option.keyedEmpty, that creates the option with all keys, and associated values set to Nothing?

Sorry, I'm a little confused. What would be the difference between keyedEmpty and empty? Like, what would the type of keyedEmpty be? Higher-level, what are you wanting to do that's hard?

@bbarker
Copy link
Contributor Author

bbarker commented Sep 6, 2019

The pattern is to Add a type parameter!:

That's super clever!

Sorry, I'm a little confused. What would be the difference between keyedEmpty and empty? Like, what would the type of keyedEmpty be?

empty and keyedEmpty would be the same type forall option. Option option, I think.

Higher-level, what are you wanting to do that's hard?

The difference between empty and keyedEmpty would be that the values they give would be different. empty is truly empty, with no keys added yet. This is significant since set (and other functions in the API) depend on what keys are already present ("The key must already exist in the option."). It would be really convenient to just be able to use set to update my record without worrying about this constraint, in my particular use case.

The reason is this: i'm looping over a value of type option with many fields. The first iteration, i could use insert, and all subsequent iterations could use set. But it is not very nice to have different behavior in different iterations in a recursive solution.

Of course, I may be misunderstanding some existing functionality, but hopefully I've done a better job explaining today now that I'm more well rested!

@joneshf
Copy link
Owner

joneshf commented Sep 6, 2019

If I'm understanding your particular case correctly, you'll want to use Option.set the whole time. Using Option.insert, then Option.set seems as though you've got a record and you want to control the fields statically. That's fine, but it's a bunch of extra work. Instead, you should be able to create an option with the keys you expect it to have (using Option.empty) and do your looping:

baz :: Option.Option ( foo :: Int, bar :: String )
baz = Option.empty

qux :: Option.Option ( foo :: Int, bar :: String )
qux = Option.set (Data.Symbol.SProxy :: _ "foo") 31 baz

This might be a documentation issue. The difference between insert and set is what they do to the type of an Option _, not what they do to a value of type Option _. insert will always change the type to have more keys. set will not change which keys are in an Option _.

E.g.
If we have an Option _:

start :: Option.Option ( foo :: String )
start = Option.empty

We cannot insert foo:

-- Will have an error about duplicate rows or types not unifying or something.
this_will_not_type :: Option.Option ( foo :: String )
this_will_not_type = Option.insert (Data.Symbol.SProxy :: _ "foo") "test" start

The foo key is already in the type of start. Whether or not the foo key exists in start is irrelevant to Option.insert because it's expecting start's type to not have the foo key. It won't type check.

We can set foo though:

-- No errors
end :: Option.Option ( foo :: String )
end = Option.set (Data.Symbol.SProxy :: _ "foo") "test" start

We can do this because the foo key is already in the type of start. Whether the foo key exists in start is irrelevant to Option.set, it's always going to replace it with the value "test".

I don't think it's possible to have one value that behaves the way both Option.insert and Option.set behave while still retaining type safety.

The reason Option.insert and Option.set are named the way they are is to parallel the values Record.insert and Record.set, respectively. In fact, most of Option has parallels to Record.

Do you have any suggestions on what might make the distinction between Option.insert and Option.set clearer?

@joneshf
Copy link
Owner

joneshf commented Sep 6, 2019

Oh no! I just looked at the documentation for Option.empty. The example there is incorrect. Sorry about that. Maybe that's what's causing the confusion? I'll update that and see if I've any more documentation issues.

@bbarker
Copy link
Contributor Author

bbarker commented Sep 6, 2019

@joneshf Great to hear - I think your doc fix for the empty example helps clarify. But there remains one point of confusion for me in the docs related to empty:

Creates an option with no key/values that matches any type of option.

I think this might be more unambiguous if it read:

Creates an option with key/value pairs such that the keys are from the Option type option and values are all Nothing

Of course, I may still be misunderstanding something, but I think the Option must have keys for it to work with set, and this seems to agree with the changes you made and your post above.

@bbarker
Copy link
Contributor Author

bbarker commented Sep 7, 2019

The pattern is to Add a type parameter!:

That's super clever!

Sorry, I'm a little confused. What would be the difference between keyedEmpty and empty? Like, what would the type of keyedEmpty be?

empty and keyedEmpty would be the same type forall option. Option option, I think.

Higher-level, what are you wanting to do that's hard?

The difference between empty and keyedEmpty would be that the values they give would be different. empty is truly empty, with no keys added yet. This is significant since set (and other functions in the API) depend on what keys are already present ("The key must already exist in the option."). It would be really convenient to just be able to use set to update my record without worrying about this constraint, in my particular use case.

The reason is this: i'm looping over a value of type option with many fields. The first iteration, i could use insert, and all subsequent iterations could use set. But it is not very nice to have different behavior in different iterations in a recursive solution.

Of course, I may be misunderstanding some existing functionality, but hopefully I've done a better job explaining today now that I'm more well rested!

@bbarker bbarker closed this as completed Sep 7, 2019
@bbarker
Copy link
Contributor Author

bbarker commented Sep 9, 2019

Apparently I closed this issue, which was not my intention

@bbarker bbarker reopened this Sep 9, 2019
@joneshf
Copy link
Owner

joneshf commented Sep 21, 2019

No worries.

Creates an option with key/value pairs such that the keys are from the Option type option and values are all Nothing

Your suggestion points toward different terminology making it clearer what these things are used for. Maybe the issue is that the terminology is too loose and a more explicit distinction between run-time and compile-time would be helpful?

@bbarker
Copy link
Contributor Author

bbarker commented Sep 24, 2019

@joneshf Yes, I think you are right about the terminology maybe being possible to be clearer distinguishing between values and types, but I think the doc fix update PR is good as is. I can take a stab at trying to update the docs in a second pass based on my improved understanding, and hopefully that will also help fill in some remaining gaps, if there are any.

Regarding coming up with an analogue to sequence as discussed above, this is a bit beyond my comfort zone, but wonder if using something like this could be useful?

@joneshf
Copy link
Owner

joneshf commented Sep 25, 2019

Sorry, I'm really dragging my feet on this one. I thought I'd already merged that PR 😶. I already implemented the sequence thing. I haven't documented it, so I didn't push it up anywhere. If I get some time I'll try to throw something together soon for it.

@bbarker
Copy link
Contributor Author

bbarker commented Oct 1, 2019

@joneshf no worries, but wanted to ping back on this as I'm now in a good place to try out the sequence function in my code base. If you have time to push it as-is to another branch or something, I could test it out somehow

joneshf added a commit that referenced this issue Oct 9, 2019
As requested in #2,
we'd like some way to get every value if they all exist. If anything
doesn't exist, we'd like the whole thing to fail.

This is similar in spirit to `Data.Traversable.sequence`, but
monomorphized to `Data.Maybe.Maybe` as the `Applicative`. Since it also
shares quite a bit in common with `get`, we name it accordingly.

This naming hints that there might be more to the family of get* values.
We might think about a way to get a subset of keys from an option. Or,
we might want a way to get a subset of keys with default values.
Something to think about for the future.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants