goretkin / FixArgs.jl

Other
19 stars 3 forks source link

Do not use nothing for holes? #2

Open jw3126 opened 4 years ago

jw3126 commented 4 years ago

Instead of using nothing to indicate holes, we could use

struct Hole end
const hole = Hole()
const ◻ = hole

So we don't have to think about escaping Some(nothing) and also have more cute notation like: @bind ◻ == 1

goretkin commented 4 years ago

I think it's a good idea to consider using a singleton other than nothing to indicate a hole, and your suggestion looks reasonable.

But whichever value is chosen, there should always be a way to escape it. We can escape it with Some, too.

jw3126 commented 4 years ago

You are right an escape mechanism is good to have either way. However, I expect nothing would be needed frequently while a singelton would almost never be needed to escaped.

tkf commented 4 years ago

Why not do this at the macro level? @bind f(nothing, _) can be lowered to bind(f, Some(nothing), nothing).

jw3126 commented 4 years ago

@tkf do you propose to keep nothing for indicating holes?

tkf commented 4 years ago

Yeah, I think that's the most robust solution. For example, if you have

mapobjects(f, m::Module) = (f(getfield(m, n)) for n in names(m))

and use bind inside f, it's better that mapobjects(f, Curry) doesn't do something weird. With , something like

f(x) = count(bind(isa, x, ◻), [Integer, Symbol])

could break it.

jw3126 commented 4 years ago

Maybe I don't fully get the example. I do see that it has possibly surprising behavior. But the same is true with nothing instead of hole when operating a module that exports nothing (e.g. Base) or not?

I also with hole it is more natural to add another struct splathole to allow e.g. @bind +(1, hole...)

tkf commented 4 years ago

I think it's more about Some{T}-vs-T than nothing-vs-hole. For example, if I have

g(x) = count(bind(isa, Some(x), nothing), [Integer, Symbol])

instead then the first argument of isa is always bound while it's not the case for f(◻).

jw3126 commented 4 years ago

@goretkin suggested hole could also be escaped with Some. Then g works for nothing/hole equally right?

tkf commented 4 years ago

I thought the point of hole is that you can write bind(f, x, hole) instead of bind(f, Some(x), hole) or bind(f, Some(x), nothing). It's not super satisfactory if you can't use bind(f, x, hole) with 100% confidence when x could be a hole.

Sometimes there is no easy way to avoid "sentinel" values/types like hole. But here, don't @bind f(x, _) solve the issue that it's ugly to have Some(nothing)?

goretkin commented 4 years ago

I thought the point of hole is that you can write bind(f, x, hole) instead of bind(f, Some(x), hole)

No, I think the point of hole is to have a less common value than nothing to escape.

The point of hole is that you can write bind(f, nothing, hole)

instead of bind(f, Some(nothing), nothing)

tkf commented 4 years ago

That's what I meant.

goretkin commented 4 years ago

I think we're discussing two separate issues, then.

  1. which sentinel value to use, nothing or hole.
  2. the benefits of using a macro and _ to indicate holes.

If you do not use a macro, then you can never safely do bind(f, x, sentinel), independent of the sentinel value used, because x could be the sentinel.

If you use a macro, then you can use _ to indicate holes and you can write @bind f(x, _) and not worry about escaping x. This works because _ can not be used as an rvalue, unlike (\square)

I don't understand what @tkf is illustrating in https://github.com/goretkin/Curry.jl/issues/2#issuecomment-649826727 , however, and why nothing is more robust than hole. What is the difference between

lowering @bind f(nothing, _) to bind(f, Some(nothing), nothing) or bind(f, nothing, other_sentinel)

or lowering @bind f(◻, _) to bind(f, ◻, nothing) or bind(f, Some(◻), ◻)

I don't see a privileged choice of sentinel, except if for convention that Some probably exists to escape nothing, so it seems like nothing is a good choice to stick with.

tkf commented 4 years ago

why nothing is more robust than hole

I never tried to argue this. My main claim has been that "it's more about Some{T}-vs-T than nothing-vs-hole."

I don't see a privileged choice of sentinel, except if for convention that Some probably exists to escape nothing, so it seems like nothing is a good choice to stick with.

Union{Nothing,Some{T}} is the canonical optional value in Julia. This "convention" is important because you get a common set of tools to work with this type (e.g., something). Or maybe in the future we'll get f?(x) to automatically lift f(::T) to Union{Nothing,Some{T}}. These things are important when you want to build arguments for bind programmatically.

goretkin commented 4 years ago

I agree with all of that. I was responding perhaps to ”Yeah, I think that's the most robust solution. For example, ...”.

On Fri, Jun 26, 2020, 9:50 PM Takafumi Arakaki notifications@github.com wrote:

why nothing is more robust than hole

I never tried to argue this. My main claim has been that "it's more about Some{T}-vs-T than nothing-vs-hole."

I don't see a privileged choice of sentinel, except if for convention that Some probably exists to escape nothing, so it seems like nothing is a good choice to stick with.

Union{Nothing,Some{T}} is the canonical optional value in Julia. This "convention" is important because you get a common set of tools to work with this type (e.g., something). Or maybe in the future we'll get f?(x) to automatically lift f(::T) to Union{Nothing,Some{T}}. These things are important when you want to build arguments for bind programmatically.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/goretkin/Curry.jl/issues/2#issuecomment-650472039, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAEN3M5JH55PQCGVRAEJ37DRYVF77ANCNFSM4OAVN2JQ .

tkf commented 4 years ago

I see. Yes, that part sounds like nothing has something better inherently by itself.

To sum-up, my arguments are:

  1. No sentinel value is safe. We need some wrapper like like Some{T}.
  2. Union{Nothing,Some{T}} has an extra benefit as it's the canonical optional value representation.
  3. If we need Some (or alike) anyway, let's just make it convenient (hence macro).
goretkin commented 4 years ago

@jw3126 do you think this is okay to close?

jw3126 commented 4 years ago

I would prefer to keep it open. I am still favoring hole. I do prefer fumctions over macros and with hole using the fix function is much less dangerous. Also

I also with hole it is more natural to add another struct splathole to allow e.g. @bind +(1, hole...)

goretkin commented 4 years ago

I agree it's less dangerous, but the danger is present regardless of the choice of sentinel values. It seems like good practice would always require Some(...) in order to leave room for nothing or hole, otherwise you build in the assumption that some value is never nothing nor hole.

So given that thinking, I agree with @tkf that it's worth using the "canonical optional value" representation.

To elaborate............. The problem of signalling things "in band", and therefore needing "escaping" shows up many times: regexes, format strings, markup languages. In those case, most of the time you're not doing escaping. When you write a regex, most of the time you're matching literals, so you don't have to escape string literals. You write those as they are. Some strings need escaping, because their meaning is taken up by the regex syntax.

The opposite choice is to escape string literals. That's what e.g. https://github.com/jkrumbiegel/ReadableRegex.jl allows you to do, in addition to giving descriptive names for all the regex operators.

I think to write safe, modular code, you can't assume the absence of a sentinel value if the value is coming from another module, that you'd like to not really know much about. So the only way to do that in the realm of Julia values is to ensure that anything that could take on a sentinel value but not mean a sentinel value be wrapped in something that adds a layer of "This value does not have the meaning of a sentinel value".

Many uses of this package probably don't have values coming from other modules. The fixed (/ bound) values are literals in the code. In that case it feels very safe to get away without writing Some.

But in the spirit of having both convenience and orthogonality (so you can replace a literal by a variable reference, and now there's a risk one day it will=== sentinel value), you now cannot avoid using a macro. Using a macro lets you be concise and safe, otherwise you have to escape a lot of stuff with Some.

And so then since you're using a macro anyway, then why not choose the pattern that Julia uses elsewhere, so that we do not need to invent yet another "escaping scheme"?

And the only way hole could really ever show up somewhere "by surprise" is if you were fixing while you were fixing. Which I definitely cannot think of an application for, but packages are powerful when they have compositionality.

We could just say "when you're doing this weird thing of fix(fix(f, ...), ...), be wary of escaping". And it might never come up with someone, but if it ever does 1. I think they won't need to worry about escaping if they're using the macros, 2. if they don't want to use macros, they will be able to leverage standard patterns in Julia for handling the escaping.

That was much longer elaboration than I intended, but that's how I would defend the choice to use nothing as the sentinel value.

with hole it is more natural to add another struct splathole to allow e.g. @bind +(1, hole...)

I think that is a good feature, and I don't exactly see what's less natural about using splathole and nothing vs splathole and hole. The aesthetics don't look that great, but you could define a singleton SplatNothing or SlurpNothing or something else that might generally mean "as many missing things as you need to make this make sense", and that might even be a useful notion in another package. That's far-fetched, but I bring it up in case it resonates.

Also, by the way, we could borrow from https://github.com/FluxML/MacroTools.jl/blob/e43325c939cfe7bbb02900a9695309af4d14d1a2/docs/src/pattern-matching.md#L55

Symbols like f__ (double underscored) are similar, but slurp a sequence of arguments into an array. For example:

MasonProtter commented 4 years ago

I think you should definitely define your own singleton for this package to use rather than one from Base. While no sentinel value is safe, using your own homemade sentinal value is way safer than one from Base.

hole was suggested, but I think I'd prefer free (as in the free, or unfixed argument) , i.e. fix(+, free, 1) means x -> x + 1.

I think it's really nice that this package doesn't need a macro to have nice syntax. I'd rather something like this just took advantage of multiple dispatch.

tkf commented 4 years ago

You don't need to use any sentinel if you store Union{Some,Nothing}, though?

goretkin commented 4 years ago

@MasonProtter if you can provide examples of other packages, or Base defining singletons that serve as sentinels, I think it would help the discussion.

MasonProtter commented 4 years ago

You don't need to use any sentinel if you store Union{Some,Nothing}, though?

Doesn't the user then have to write Some all over the place though, generating way more boiler plate than this package would save? Or is there a way to avoid the user needing to write Some?

@MasonProtter if you can provide examples of other packages, or Base defining singletons that serve as sentinels, I think it would help the discussion.

I'm not sure I understand what you're asking for. Are you just asking about examples of packages that define singletons for directing dispatch?

tkf commented 4 years ago

Doesn't the user then have to write Some all over the place though, generating way more boiler plate than this package would save?

@MasonProtter I don't think it's a good approach to sacrifice robustness just for convenience. IMHO it's very important to get a rigorous API first and then think about the syntax sugar.

Using @fix macro solves explicit Some problem completely. Furthermore, you don't need yet another sentinel at the surface syntax for encoding splatting because you can just write @fix f(1, 2, _...).

if you can provide examples of other packages, or Base defining singletons that serve as sentinels,

@goretkin Actually, https://github.com/JuliaFolds/InitialValues.jl (e.g., INIT) and https://github.com/JuliaFolds/Transducers.jl (e.g., Reduced) heavily relying on sentinels/special types that have similar problem with hole discussed here. I'm not 100% happy with this but it's like this due to a mixture of performance, compatibility, and historical reasons.

MasonProtter commented 4 years ago

If you're using a macro for the surface syntax, why do you need Some at all? Just make the macro directly construct the Fix object.

tkf commented 4 years ago

Fix as implemented currently uses Some to encode holes. This is also required for using Fix for dispatch. Also, Some is useful for fix function where you can build arguments programmatically.

I think it is reasonable to argue that all of these can use a mechanism other than Some. However, sentinel is not appropriate for any of them because there is a chance that the value to be fixed is the sentinel itself.

goretkin commented 4 years ago

If you're using a macro for the surface syntax, why do you need Some at all? Just make the macro directly construct the Fix object.

I think @tkf answered the question, but perhaps @MasonProtter has a different implementation in mind. If so, could you show what you mean in terms of how would you directly construct the Fix object to be equivalent to x -> print(nothing, x) ?

However, sentinel is not appropriate for any of them because there is a chance that the value to be fixed is the sentinel itself.

This might be a pedantic point, and I may be wrong about it, but I was considering the value nothing to be a sentinel, and it is fine to use it as such because you can escape it with Some. Other values don't need to be escaped with Some, but could be.

e.g.

julia> FixArgs.fix(print, nothing, Some(:a))(:b)
ba
julia> FixArgs.fix(print, nothing, :a)(:b)
ba

You can escape nothing:

julia> FixArgs.fix(print, nothing, Some(nothing))(:b)
bnothing

And you can also escape a Some:

julia> FixArgs.fix(print, nothing, Some(Some(:a)))(:b)
bSome(:a)

One design change that we could make is to make FixArgs.fix(print, nothing, :a) invalid. Every value would be a Union{Nothing, Some{T}}. I don't see a benefit to doing this.

tkf commented 4 years ago

One design change that we could make is to make FixArgs.fix(print, nothing, :a) invalid. Every value would be a Union{Nothing, Some{T}}. I don't see a benefit to doing this.

I don't have a strong opinion on this but I think it's OK to "cast" T to Some{T} automatically by fix if T != Nothing. This could be still useful if you are sure that T is not Nothing (e.g., literal like :a).

But I think that it's better to normalize the representation when constructing Fix. This way, the consumers of Fix object only have to dispatch on Some{T} and not Union{T,Some{T}}.

goretkin commented 3 years ago

I've played around with using a type to index into the positional arguments of a function, instead of just using nothing in the corresponding position:

https://github.com/goretkin/FixArgs.jl/blob/35722303c7886630ae42537eaaf2f04e8089b374/src/FixArgs.jl#L139-L140

If that solution pans out, then indeed the idea will not use nothing for holes. This makes the idea a bit more complicated, but I want to consider it.

goretkin commented 3 years ago

I've played around with using a type to index into the positional arguments of a function, instead of just using nothing in the corresponding position:

I've taken this idea pretty far, and effectively "holes" are not represented with ::Nothing but instead with types defined in the package.