Parameterized Keywords

Idan Arye via Digitalmars-d digitalmars-d at puremagic.com
Tue Mar 8 09:35:16 PST 2016


On Monday, 7 March 2016 at 05:56:54 UTC, Patience wrote:
> Just curious if anyone can see the use for them?
>
> e.g., for[32], switch[alpha] //alpha is a user type, if[x](x < 
> 32)
> etc..
>
> The idea is two-fold, one is to allow polymorphic keyword 
> behavior(it's behavior depends on the argument) and to allow 
> the code itself to manipulate the behavior.
>
> Of course, the behavior isn't easily define for the above 
> examples. So far all I can think of is various simple 
> modifications:
>
> foreach[?] <- executes only if there are one or more iterations 
> and checks for nullity. This allow one to avoid writing the if 
> check.
>
> int[size] <- creates an integer of size bits.
>
> etc...
>
> I can't see much benefit but maybe someone has some good uses?
>
> These could be thought of as "attributes for keywords". The 
> programmer would have to define precisely what if[x] means.
>
> Obviously the biggest objection is "It will obfuscate 
> keywords"... Please don't obfuscate the question with such 
> responses.

This... looks like you have a solution and now you are looking 
for a problem. I fail to see the need to parameterize the 
*keywords*, when what you really want to modify is the 
*constructs* created from these keywords. That is, you don't want 
to modify `if` keyword - you want to modify the whole `if` 
statement(`if (a) { b(); } else { c(); }`). Modifying the keyword 
is just a means to that end.

This distinction is important because the term "keywords", while 
distinctive and important at the lexical phase, is too broad at 
the grammar phase and is no longer meaningful once we reach the 
semantics phase. Parameterizing `int` is different from 
parameterizing `foreach` is different from parameterizing `pure`.

Of course, this statement doesn't hold for homoiconic languages, 
where keywords are actual values and parameterizing them simply 
means returning a different value. Also, I'm assuming you mean to 
allow defining parameterizations at library level - otherwise 
they won't be very useful, since you could simply create new 
syntactic constructs.

So, assuming you the language is not homoiconic and that users 
and library authors should be able to define("overload") their 
own keyword parameterizations, the keywords will need to be 
partitioned into several categories: data-types, annotations, 
statements etc. Each category should have it's own 
parameterization overloading rules - so `int[...]` and 
`float[...]` will have similar rules, which will be very 
different from `if[...]`'s rules.

Now, let's focus, for a moment, on types - because 
"parameterizing" types is a solved problem - it's called 
"templating". You usually want parameterized types to also be 
types - your `int[12]` should be a type, usable wherever types 
are usable - which is exactly what templated types do - it's easy 
to implement `CustomSizedInt!12` which does exactly what your 
`int[12]` does. In fact, templated types are better, because:

  1) When you encounter `CustomSizedInt!12` and want to know what 
it does, you need to search for `CustomSizedInt`'s declaration - 
an extremely common problem, automated by many simple-to-use IDE 
features and command line tools. To divine `int[12]`'s meaning 
you'd have to look for the implementation of a the 
parameterization of `int` with another `int`(or with a `long`, or 
with a `uint`, or with a...), and you need a more complex query 
to search for it.

  2) `int[12]` is a user defined type, but it's conceptually 
coupled to `int`. The mere concept of parametrized keyword types 
is coupled to primitive types. Templated types do not have this 
limitation - they can depend on whatever they want to - so you 
have much more freedom. Even if parameterized keywords could do 
everything templated types could, you'd have to abuse 
them(resulting in many code smells) whenever you want a type 
doesn't strictly resolve around a primitive type - something that 
comes naturally with templated types.

  3) Code that invokes user-defined behavior should have an 
"anchor" - something the definition of that behavior resolves 
around. When you call a function it's the function. When you call 
a method it's the object's type. When you use an overloaded 
operator it's the type of one of the operands. When you use 
`int[12]`? Both `int` and `12` are built in - there is no anchor. 
If `int[12]` is to be library defined, it would have to be more 
like `int[SizeInBits(12)]`(so `SizeInBits` is the anchor), and 
suddenly it doesn't look that syntactically appealing compared to 
`CustomSizedInt!12`...


So, that was for keywords that represent types - what about other 
keywords? I picked types because it's something D(and many other 
languages) already have, but I claim that the same reasons apply 
to all other keywords. Let's look at another easy one - 
annotations. Let's say you want to paramererize `pure` - e.g. 
`pure[MyPureModifier]` - so it'll do something a bit different. 
It'll still be an annotation, so it'll have to annotate some 
declaration. If you are already going to implement custom 
modification of declarations, why not give that power to UDAs? 
`@MyPure` looks better, has a clear anchor, it's definition is 
easier to search, and it doesn't have to be related to an 
existing modifier with and existing purpose.


What about more complex stuff, like your `foreach[?]`? Many would 
suggest macros, but even without going to hyperblown macroland, 
identifiers are better than parameterized keywords. Ruby has a 
nice syntax for it with blocks. Instead of parameterizing the 
existing `for` into `for[?]`, it lets me define my own "keyword" 
- `foreach?`:



[1] pry(main)> for x in [1, 2, 3]
[1] pry(main)*   puts "Loop1: #{x}"
[1] pry(main)* end
Loop1: 1
Loop1: 2
Loop1: 3
=> [1, 2, 3]
[2] pry(main)> for x in nil:
SyntaxError: unexpected ':', expecting keyword_do_cond or ';' or 
'\n'
[2] pry(main)> for x in nil
[2] pry(main)*   puts "Loop2: #{x}"
[2] pry(main)* end
NoMethodError: undefined method `each' for nil:NilClass
from (pry):4:in `__pry__'
[3] pry(main)> def foreach?(collection)
[3] pry(main)*   if collection
[3] pry(main)*     for entry in collection
[3] pry(main)*       yield entry
[3] pry(main)*     end
[3] pry(main)*   end
[3] pry(main)* end
=> :foreach?
[4] pry(main)> foreach? [1, 2, 3] do|x|
[4] pry(main)*   puts "Loop 3: #{x}"
[4] pry(main)* end
Loop 3: 1
Loop 3: 2
Loop 3: 3
=> [1, 2, 3]
[5] pry(main)> foreach? nil do|x|
[5] pry(main)*   puts "Loop 4: #{x}"
[5] pry(main)* end
=> nil



The main idea in all these examples is the same - instead of 
parameterizing the existing keywords and syntax, it's better to 
have a generic syntax designed for overloading by library and 
user code.


More information about the Digitalmars-d mailing list