www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Type inference for default function / method arguments?

reply Witold Baryluk <witold.baryluk gmail.com> writes:
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:

```d
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:



```d
// 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:

```d
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:

```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) {
...
}
```

(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:


```d
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.

```d
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:

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

would be semi-equivalent to template like this:

```d
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

```crystal
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:

```dart
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 ):


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

...

   $type(test);     // (?i : Int, ?s : String) -> String
```
May 10 2021
next sibling parent reply Imperatorn <johan_forsberg_86 hotmail.com> writes:
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
parent Witold Baryluk <witold.baryluk gmail.com> writes:
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
prev sibling next sibling parent reply SealabJaster <sealabjaster gmail.com> writes:
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"; 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
parent reply Andrei Alexandrescu <SeeWebsiteForEmail erdani.org> writes:
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 11 2021
next sibling parent rikki cattermole <rikki cattermole.co.nz> writes:
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
prev sibling parent SealabJaster <sealabjaster gmail.com> writes:
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
May 11 2021
prev sibling next sibling parent "H. S. Teoh" <hsteoh quickfur.ath.cx> writes:
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 10 2021
prev sibling parent Witold Baryluk <witold.baryluk gmail.com> writes:
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:

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

Here, `p` is inferred to be `int`:

```ocaml
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:

```ocaml
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:

```ocaml

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

```ocaml
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