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