Thread overview
Type inference for default function / method arguments?
May 10, 2021
Witold Baryluk
May 10, 2021
Imperatorn
May 10, 2021
Witold Baryluk
May 10, 2021
SealabJaster
May 11, 2021
rikki cattermole
May 11, 2021
SealabJaster
May 10, 2021
H. S. Teoh
May 11, 2021
Witold Baryluk
May 10, 2021

Hi,

using D for very long time.

I was working on a project, and converting bunch of optional bool options in various functions, to use Flag and BitFlag, to make it more readable, especially on a call side.

Before:

void show_help_and_exit(const string exec_name, const string extra_help,
                        bool verbose = false,
                        bool reallyverbose = false,
                        bool shorthelp = false) {
...
}


...
show_help_and_exit(exec_name, extra_help, /*verbose=*/true, /*reallyverbose=*/true);

So, now with Flag, it could look like this:

// rename import, because of collision with my "Flag" classes
import std.typecons : ArgFlag = Flag, Yes, No;

void show_help_and_exit(const string exec_name, const string extra_help,
                        ArgFlag!"verbose" verbose = No.verbose,
                        ArgFlag!"reallyverbose" reallyverbose = No.reallyverbose,
                        ArgFlag!"shorthelp" shorthelp = No.shorthelp) {
...
}

...
show_help_and_exit(exec_name, extra_help, Yes.verbose, Yes.reallyverbose);

Which is great on a caller side, but is very verbose on the function (or method) definition side.

I wish there was a shorter way to express this function declaration or definition.

The only other option, that is a bit nicer, but not actually shorter is:

import std.typecons : Yes, No;

void show_help_and_exit(const string exec_name, const string extra_help,
                        typeof(No.verbose) verbose = No.verbose,
                        typeof(No.reallyverbose) reallyverbose = No.reallyverbose,
                        typeof(No.shorthelp) shorthelp = No.shorthelp) {
...
}

Really, I would like to be able to say this:

import std.typecons : Yes, No;

void show_help_and_exit(const string exec_name, const string extra_help,
                        auto verbose = No.verbose,
                        auto reallyverbose = No.reallyverbose,
                        auto shorthelp = No.shorthelp) {
...
}

(with possibly extra like const, ref, etc).

For a lot of stuff (like Flag), it is quite clear what is the type. And IDEs can also help here a lot.

I can't use the template here, because it would mess things up quite a bit, if the caller passes a wrong type, and because all the 3 above "flags" are own types, so this will not work:

import std.typecons : Yes, No;

void show_help_and_exit(F)(const string exec_name, const string extra_help,
                           F verbose = No.verbose,
                           F reallyverbose = No.reallyverbose,
                           F shorthelp = No.shorthelp) {
...
}

And also templates will not work when using simple strategy for methods of classes or interfaces.

The auto would be also useful in other cases, for example, when using Options structs.

struct Options {
  int a;
  string b;
  string c;
}

void run(string name, auto options = Options()) {
}

One, could even argue that this should apply not just to default argument (which can be handled using standard type inference), but all argument:

auto f(auto x, auto y) {
  return g(x + y);
}

would be semi-equivalent to template like this:

auto f(X, Y)(X x, Y y) {
  return g(x + y);
}

The only problem with that, is you would not be able to easily take address of such "function", or instantiate it explicitly to pass somewhere, so that probably is a bad idea long term.

But, for the normal type inference of the default arguments, everything else should work just fine. They can be functions, delegates, they have concrete type, can be taken address of, passed around, etc.

PS. Note that my "proposal" doesn't influence in anyway other discussions about the function arguments, like out-of-order named arguments, specifaying some out-of-order default arguments, etc.

There is not many other statically typed modern languages with type inference and this feature (I checked about 10 different ones). I found some tho.

First is "Crystal" (inspired by Ruby), https://crystal-lang.org/reference/syntax_and_semantics/type_inference.html#5-assigning-a-variable-that-is-a-method-parameter-with-a-default-value

class Person
  def initialize(name = "John Doe")
    @name = name
  end
end

will infer locally name argument variable to be String, and so would be @name (member variable of the class).

It is also possible in Dart. Example:

String f(int a, {b = "bzium"}) {
  return '${a} - ${b}';
}

void main() {
  print(f(5));
  print(f.runtimeType.toString());  // (int, {dynamic b}) => String
}

It does work, but is not exactly equivalent to String f(int a, {String b = "bzium"}), because at the moment Dart compiler infers b to dynamic, instead of String, but it could be just deficiency of the compiler or specification on their side.

And Haxe ( https://haxe.org/manual/types-function-default-values.html ):

  static function test(i = 12, s = "bar") {
    return "i: " + i + ", s: " + s;
  }

...

  $type(test);     // (?i : Int, ?s : String) -> String
May 10, 2021

On Monday, 10 May 2021 at 18:50:02 UTC, Witold Baryluk wrote:

>

Hi,

using D for very long time.

I was working on a project, and converting bunch of optional bool options in various functions, to use Flag and BitFlag, to make it more readable, especially on a call side.

You could maybe use Variant somehow?

May 10, 2021

On Monday, 10 May 2021 at 19:32:19 UTC, Imperatorn wrote:

>

On Monday, 10 May 2021 at 18:50:02 UTC, Witold Baryluk wrote:

>

Hi,

using D for very long time.

I was working on a project, and converting bunch of optional bool options in various functions, to use Flag and BitFlag, to make it more readable, especially on a call side.

You could maybe use Variant somehow?

Could you elaborate? I am not sure how would it help in anyway.

May 10, 2021

On Monday, 10 May 2021 at 18:50:02 UTC, Witold Baryluk wrote:

>

Hi,

using D for very long time.

While not an exact solution, aliases can be a little useful here.

import std.typecons : Flag;

// Shorten the names as much as you need of course.
alias Verbose = Flag!"verbose";
alias VeryVerbose = Flag"veryVerbose";

void func(Verbose verbose = Verbose.yes, VeryVerbose veryVerbose = VeryVerbose.no)
{
}

And you can of course do that for other types, e.g. alias Opt = Options

But again, this is more a workaround for why you're wanting the auto inference.

May 10, 2021
On Mon, May 10, 2021 at 06:50:02PM +0000, Witold Baryluk via Digitalmars-d wrote: [...]
> Really, I would like to be able to say this:
> 
> ```d
> import std.typecons : Yes, No;
> 
> void show_help_and_exit(const string exec_name, const string extra_help,
>                         auto verbose = No.verbose,
>                         auto reallyverbose = No.reallyverbose,
>                         auto shorthelp = No.shorthelp) {
[...]

I've also felt the need for this. Since the default value already implies the type, it seems to be a violation of DRY to have to type it out, especially for enums where you already have to type the name of the enum.

IMO, this deserves to be a DIP. It's useful, self-contained, and probably quite easy to implement.


T

-- 
There are two ways to write error-free programs; only the third one works.
May 11, 2021

Also OCaml supports inference for default arguments, but considering very powerful type inference solver in Ocaml, that is kind of cheating too. https://caml.inria.fr/pub/docs/u3-ocaml/ocaml051.html Example:

let substring ?pos:(p=0) ~length:l s = String.sub s p l;;

Here, p is inferred to be int:

val substring : ?pos:int -> length:int -> string -> string = <fun>

It inferred to be a for two reasons: default value (0) is an int, and String.sub second argument is supposed to be an int. However, it works even if the p is not used, and only the default value is used for initialization:

let f ?x:(x=42) = x;;
Warning 16: this optional argument cannot be erased.
val f : ?x:int -> int = <fun>

x is inferred to be an int.

This is different that, without default argument:

# let f ?x:x = x;;
Warning 16: this optional argument cannot be erased.
val f : ?x:'a -> 'a option = <fun>

Where f is a generic function, working with any type.

or

let rec list_map f ?(accum = []) l = match l with
    | head :: tail -> list_map f ~accum:(f head :: accum) tail
    | [] -> List.rev accum;;
val list_map : ('a -> 'b) -> ?accum:'b list -> 'a list -> 'b list = <fun>

accum is inferred to be a list.

I think it is pretty handy, and brings D a bit closer to more compact format, know from places like Ocaml, Haskell, or dynamic languages like Python.

May 11, 2021
On 5/10/21 4:01 PM, SealabJaster wrote:
> On Monday, 10 May 2021 at 18:50:02 UTC, Witold Baryluk wrote:
>> Hi,
>>
>> using D for very long time.
> 
> While not an exact solution, aliases can be a little useful here.
> 
> ```d
> import std.typecons : Flag;
> 
> // Shorten the names as much as you need of course.
> alias Verbose = Flag!"verbose";
> alias VeryVerbose = Flag"veryVerbose";

That's an antipattern. The very point of Flag is to allow avoiding polluting the namespace and docs with such silly names.

Similarly, you wouldn't define an alias for every Tuple you use.

May 12, 2021
On 12/05/2021 1:43 AM, Andrei Alexandrescu wrote:
> On 5/10/21 4:01 PM, SealabJaster wrote:
>> On Monday, 10 May 2021 at 18:50:02 UTC, Witold Baryluk wrote:
>>> Hi,
>>>
>>> using D for very long time.
>>
>> While not an exact solution, aliases can be a little useful here.
>>
>> ```d
>> import std.typecons : Flag;
>>
>> // Shorten the names as much as you need of course.
>> alias Verbose = Flag!"verbose";
>> alias VeryVerbose = Flag"veryVerbose";
> 
> That's an antipattern. The very point of Flag is to allow avoiding polluting the namespace and docs with such silly names.
> 
> Similarly, you wouldn't define an alias for every Tuple you use.

Once named arguments is implemented we can get rid of this crutch.

Binary Flag needs to go!
May 11, 2021
On Tuesday, 11 May 2021 at 13:43:26 UTC, Andrei Alexandrescu wrote:
> That's an antipattern. The very point of Flag is to allow avoiding polluting the namespace and docs with such silly names.

Y... yea... who'd do such a thing...

https://i.imgur.com/IFZbMRy.png