[wish] "non-simple" parameter defaults and constants, cont'd

Discuss the future of the AutoHotkey language
sirksel
Posts: 222
Joined: 12 Nov 2013, 23:48

[wish] "non-simple" parameter defaults and constants, cont'd

Post by sirksel » 11 Sep 2016, 06:55

I wasn't able to post on this thread in the archived forum, and I couldn't find it here, but I'd like to revive this discussion if possible:
https://autohotkey.com/board/topic/9077 ... -constant/

I've gone back into my code libraries to edit a bunch of functions that require default parameter sentries. Probably goes without saying, but I use these sentries where functions need to pass missing parameters on to other functions, or they may need to use empty string or 0 or the like as a legitimate parameter value (and needing something else for the missing value). I generally use an unlikely string value like "!@NA" or "`vna" or something for my sentry -- since, to my knowledge, AHK can only have default values which are simple string/numeric literals. Sentries or not, though, these are still strings...

You've made such great improvements in the integer/string distinction in v2, and enabled more predictable behaviors in a lot of my code. I've actually gone in and, where it matters, introduced type checking and exceptions, and it's working great, except for one problem... These sentries create a bunch of extra code in the type checking process to specifically look for them as a special case of string. It would be great if I could use as default parameter values a custom "class NA" that is itself just a dummy sentry object, but which is not a string, and which would allow string type checking to fail on its own without special code looking for and excluding those special magic-string sentry values.

Last time we discussed this, with some other examples about constants/defaults required with Excel automation, you very correctly summarized that I was really making two requests:
1) Add constants.
2) Allow something other than a simple literal value as a parameter's default value.
And you thought I'd made a better case for #2 than for #1. I was just wondering if you've given #2 any more thought? Of the two, that was my higher priority ask, and definitely what I'm looking to do above with the object as a default parameter value.

Also, with respect to #1, I raise these together again, as after I spent an hour wading through 10K lines of AHK trying to find a rather elusive bug... it boiled down to an issue that is related to both concepts. Some ambiguously ordered assignment/expression code (my fault!) changed one of my default parameter sentry magic-string "constants", and it broke a whole bunch of type-checking downstream.

Even if you didn't fully exploit the performance benefits of adding constants (which we discussed last time around), just having a keyword const that would cause any later reassignment to throw an exception would be very helpful. That said, I know you don't always have time for all the nice-to-haves... Thanks for your consideration though.

lexikos
Posts: 9553
Joined: 30 Sep 2013, 04:07
Contact:

Re: [wish] "non-simple" parameter defaults and constants, cont'd

Post by lexikos » 19 Jun 2020, 22:28

I'm going through old topics, and figured I'd bump this one to point out a feature added in v2.0-a112:
Unset: To accurately determine whether the caller omitted a parameter, specify unset as the parameter's default and use IsSet to determine whether the parameter has been given a value, as shown below:

Code: Select all

MyFunc(p := unset) {
    if IsSet(p)
        MsgBox "Caller passed " p
    else
        MsgBox "Caller did not pass anything"
}

MyFunc(42)
MyFunc
Source: Functions - Definition & Usage | AutoHotkey v2
This bypasses the need for a sentinel value, object or not.

I intend to add constants, and possibly default parameter initializer expressions (evaluated when the function is called with that parameter omitted), but I don't see it as a priority for v2.0. v2.0-a112 adds some of the "architecture" for constants, though it's only used by classes at the moment.

sirksel
Posts: 222
Joined: 12 Nov 2013, 23:48

Re: [wish] "non-simple" parameter defaults and constants, cont'd

Post by sirksel » 21 Jun 2020, 08:45

@lexikos, thanks for this. Sounds promising! I haven't used the latest version yet (need to update lots of code for the ErrorLevel changes...), but I have a question about unset. I apologize that I haven't tried it yet. For me, I'm thinking it might only get rid of my sentinel structures if it can be passed through to a subsequent function call.

For example, lets say you have a family of functions A, B and C, where is A is the workhorse function with all the bells and whistles (many optional parameters), and B and C are simplified interfaces of A with fewer options -- that may make some preliminary calculations, set some defaults, and then make a call to A to do the work. Let's say A, B, C do all share some optional parameter P in their respective interfaces. If I leave P unset in a call to C, can it "pass through" that unset state when it ultimately calls A to do the work? (I can do this with a sentinel, but I'm wondering if unset solves this.)

I also have this type of problem when calling SubStr() for example. In string functions, it seems like I can never use a single general-case call structure with a variable for the third Substr argument (length) because sometimes that third argument needs to be omitted to get all characters. I end up having to use a 2-param and 3-param version of the call embedded in a ternary structure, because there's no way to signify unset/null when passing along that third variable. Is there a way to pass "unset" in a variable from the caller's side? (Even the sentinel doesn't help me with this one, unless I just resort to writing a wrapper for SubStr and calling that instead.)

Thanks so much for all the great continued development of v2! Even with all the rework I periodically have to undertake for breaking changes, I don't think I could ever go back to v1!

User avatar
kczx3
Posts: 1640
Joined: 06 Oct 2015, 21:39

Re: [wish] "non-simple" parameter defaults and constants, cont'd

Post by kczx3 » 21 Jun 2020, 08:54

sirksel wrote:
21 Jun 2020, 08:45
Thanks so much for all the great continued development of v2! Even with all the rework I periodically have to undertake for breaking changes, I don't think I could ever go back to v1!
Agreed! It’s a pain reading v1 code anymore.

lexikos
Posts: 9553
Joined: 30 Sep 2013, 04:07
Contact:

Re: [wish] "non-simple" parameter defaults and constants, cont'd

Post by lexikos » 21 Jun 2020, 16:33

If I leave P unset in a call to C, can it "pass through" that unset state when it ultimately calls A to do the work?
No. Because P is unset, attempting to use its value causes a warning (and currently produces an empty string, i.e. the var's actual value). If the unset status was propagated through calls or assignments, forgetting to assign a value to a variable (or making a typo) would give you the parameter default. If the parameter has unset default, it would cause a warning down the line, where it's difficult to see the origin of the problem. For propagating missing a single parameter without inhibiting error detection, I think there would need to be a value or status dedicated to meaning "missing parameter" and nothing else. See also: Nothing.

You can propagate missing parameters with variadic functions/calls, but that leaves the parameter list open ended (so the "too many parameters" exception would be thrown by the nested function call rather than the original incorrect one).

Allowing a sentinel object as default would be more flexible.
... sometimes that third argument needs to be omitted to get all characters.
The maximum substring length is, I think, 2**(A_PtrSize=8 ? 63 : 31) - 1. You can assign that to a more meaningful name, like INTPTR_MAX, and pass it instead of omitting the parameter. Or you can just pass something larger than any string you deal with.

sirksel
Posts: 222
Joined: 12 Nov 2013, 23:48

Re: [wish] "non-simple" parameter defaults and constants, cont'd

Post by sirksel » 21 Jun 2020, 17:44

@Lexikos, this is great. Thanks!

I love the idea of null or Nothing. I would especially love it if it were falsey upon evaluation, since missing OR blank/zero is such a common test.

Similar to your discussion of Nothing, I currently use two sentinels: na and nl (super-global quasi-constants -- but wish they could be real constants to protect against accidental assignment -- defined as '`vna' and '`vnl'). I use the former to mean missing parameter (and nothing else), and I use the latter to mean null, not found, etc. -- for whenever I need a value other than empty string or 0 (but only when one of those won't do) to indicate such status. I considered using an object, but because of the limitation on parameter default values, the string seemed like the best possibility.

I obviously can't currently refer to these by name in function headers (and must use the literal string), but I use their names everywhere else in the code. If you do allow for constants and/or super-globals in param defaults, it would sure look a lot prettier to say myfn(x, y:=na, z:=na) than it does to say myfn(x, y:='`vna', z:='`vna'), but the latter isn't too bad... I kept the name short, because it appears thousands of times in my function headers in my library in optional parameters. I still use "" as the default parameter wherever I can get away with it (where it's not a possible legit value).

For what it's worth, the biggest issues I encounter with my current approach are the following:

1. When checking types, I have to use a special function to check strings to make sure they're not na or nl, since these shouldn't be considered strings in my code. Similarly, I can't implement special property implementations for an na or nl class/prototype/singleton to respond to property calls, because they're all really just String. In an ideal world, both would be primitives -- with the same dynamic property/method abilities as other primitives, for flexibility. They would also have a common base type with a name that meant "not a real value" (I currently use nv for that parent), so you could check for both flavors of not-a-value at the same time. Lastly, they would all evaluate falsey, so you could check for these conditions or 0/"" with a single efficient if x or easy ternary x ? y : z construct.

2. The coalescing or even Elvis operator would be great too, as it would allow for easy default setting. I currently use ifna(x, dft) and ifnl(x, dft), plus one more test for either of those "non-values": ifnv(x, dft) (as referenced in #1). The biggest issue with my current approach? NO SHORT CIRCUIT! Sometimes generating the default value might involve a lookup or something expensive. I can't really bury that in a fat arrow and have ifna check for Func/Closure, because sometimes the missing parameter is in fact intended to be of Func type. Having whatever coalescing operator use short-circuit evaluation would solve all that.

On the related point, if you implemented Nothing, would you allow it to be passed to built-ins like SubStr() too to have the same effect as omitting the parameter? By the way, I think I'll implement your max-size solution in place of the ternary structure I use. Thanks!

Also, is it likely you're going to implement something like Nothing? If so, I might decide to hold off a bit on my big edits to my library for the recent breaking changes... Thanks again!

lexikos
Posts: 9553
Joined: 30 Sep 2013, 04:07
Contact:

Re: [wish] "non-simple" parameter defaults and constants, cont'd

Post by lexikos » 21 Jun 2020, 21:32

I'm thinking that eventually objects will support overloaded operators and conversions, such as to boolean, string and number. Then you can make your own falsey sentinel.

Btw, SubStr(str, pos, (have_length ? [length] : [])*) should work. If the parameter isn't in the last position, you need to include subsequent parameters.

It's not likely that I'll implement Nothing or anything like it for v2.0.
wikipedia wrote:In certain computer programming languages, the Elvis operator, often written ?:, or or ||, is a binary operator that returns its first operand if that operand evaluates to a true value, and otherwise evaluates and returns its second operand.
We already have or/||, which behaves as described above in v2.

User avatar
vvhitevvizard
Posts: 454
Joined: 25 Nov 2018, 10:15
Location: Russia

Re: [wish] "non-simple" parameter defaults and constants, cont'd

Post by vvhitevvizard » 28 Jun 2020, 12:37

@sirksel
Thanks so much for all the great continued development of v2! Even with all the rework I periodically have to undertake for breaking changes, I don't think I could ever go back to v1!
:thumbup: Recent AHK V2's vector of changes creates the impression it is THE SCRIPTING language. King of scripting title is taken by Python at the moment but its "crutches, stretchers and wheelchairs" don't feel right and historically, popular once, it loses its traction.

@lexikos
MyFunc(p := unset) {
if IsSet(p)
Its a life saver! Sometimes, when a random numeric value has to be passed using the whole range, there was no way to check for skipped function parameters. But such a piece of code is not easily portable to other languages and (arguably) is a sign of bad coding practices. But if it doesn't deteriorate the performance why not to have it :)

lexikos
Posts: 9553
Joined: 30 Sep 2013, 04:07
Contact:

Re: [wish] "non-simple" parameter defaults and constants, cont'd

Post by lexikos » 01 Jul 2022, 20:45

v2.0-beta.6 adds some features that relate to problems discussed in this topic.

sirksel wrote:
21 Jun 2020, 08:45
For example, lets say you have a family of functions A, B and C, where is A is the workhorse function with all the bells and whistles (many optional parameters), and B and C are simplified interfaces of A with fewer options -- that may make some preliminary calculations, set some defaults, and then make a call to A to do the work. Let's say A, B, C do all share some optional parameter P in their respective interfaces. If I leave P unset in a call to C, can it "pass through" that unset state when it ultimately calls A to do the work?
Function A should define the parameters as optional with no default (p?), and then pass it to function B or function C as p?, as in A(p?) => B(p?). If function B is ultimately responsible for defining the parameter's default value or action, it can declare and use the parameter a number of ways:
  • p? and then logic within the function body defining the default value or action.
  • p := "default value"
  • p := expression (which is currently implemented by injecting p ?? p := expression at the top of the function).

I also have this type of problem when calling SubStr() for example. In string functions, it seems like I can never use a single general-case call structure with a variable for the third Substr argument (length) because sometimes that third argument needs to be omitted to get all characters. I end up having to use a 2-param and 3-param version of the call embedded in a ternary structure, because there's no way to signify unset/null when passing along that third variable. Is there a way to pass "unset" in a variable from the caller's side?
The keyword unset can now be used to signify that the parameter should be omitted. This is valid only in parameter lists (including [unset] and {prop: unset}).

lexikos wrote:
21 Jun 2020, 16:33
If the unset status was propagated through calls or assignments, forgetting to assign a value to a variable (or making a typo) would give you the parameter default.
I think this is less of an issue when the variable is explicitly marked as "maybe unset" with var?. If the parameter is mandatory, an error is thrown while the line containing typo? is still on the call stack. If some other error occurs, it is much easier to trace because it is directly visible in the parameter list.
For propagating missing a single parameter without inhibiting error detection, I think there would need to be a value or status dedicated to meaning "missing parameter" and nothing else.
Rather than dedicating a type or sentinel value for just "omit this parameter", I think the code is more robust with an explicit operator.
Allowing a sentinel object as default would be more flexible.
That's now also supported.

sirksel wrote:
21 Jun 2020, 17:44
I obviously can't currently refer to these by name in function headers
Now you can.
The coalescing or even Elvis operator would be great too, as it would allow for easy default setting.
That's also implemented, as var ?? value.

sirksel
Posts: 222
Joined: 12 Nov 2013, 23:48

Re: [wish] "non-simple" parameter defaults and constants, cont'd

Post by sirksel » 02 Jul 2022, 13:20

@lexikos... I just gotta say... you are amazing (as is beta6)! You came up with a really great way of threading the needle between the null-pointer-exception saga that comes along with with certain languages that have omnipresent nulls, and the overly complex nullable union data types in other languages that tried to get around NPEs. Unset, and its many facets you've implemented, is a really great middle ground.

Just when I thought my main script had reached Nirvana, I've got some refactoring to do. But, don't get me wrong, I'm very happy about it. What was already my favorite language, just got better again! Thanks so much for all the hard work over the years... really DECADE-plus. Your careful planning and clear direction for this language really shows through brilliantly.

Post Reply

Return to “AutoHotkey Development”