Open jw3126 opened 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.
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.
Why not do this at the macro level? @bind f(nothing, _)
can be lowered to bind(f, Some(nothing), nothing)
.
@tkf do you propose to keep nothing
for indicating holes?
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.
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...)
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(◻)
.
@goretkin suggested hole
could also be escaped with Some
. Then g
works for nothing/hole
equally right?
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)
?
I thought the point of
hole
is that you can writebind(f, x, hole)
instead ofbind(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)
That's what I meant.
I think we're discussing two separate issues, then.
nothing
or hole
. _
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.
why
nothing
is more robust thanhole
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 escapenothing
, so it seems likenothing
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.
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 .
I see. Yes, that part sounds like nothing
has something better inherently by itself.
To sum-up, my arguments are:
Some{T}
.Union{Nothing,Some{T}}
has an extra benefit as it's the canonical optional value representation.Some
(or alike) anyway, let's just make it convenient (hence macro).@jw3126 do you think this is okay to close?
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 structsplathole
to allow e.g.@bind +(1, hole...)
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:
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.
You don't need to use any sentinel if you store Union{Some,Nothing}
, though?
@MasonProtter if you can provide examples of other packages, or Base defining singletons that serve as sentinels, I think it would help the discussion.
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?
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.
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.
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.
If you're using a macro for the surface syntax, why do you need
Some
at all? Just make the macro directly construct theFix
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.
One design change that we could make is to make
FixArgs.fix(print, nothing, :a)
invalid. Every value would be aUnion{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}}
.
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:
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.
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.
Instead of using
nothing
to indicate holes, we could useSo we don't have to think about escaping
Some(nothing)
and also have more cute notation like:@bind ◻ == 1