freemarker-dev mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From "Pedro M. Zamboni" <zambonifo...@gmail.com>
Subject Re: [FM3] improve “null” handling
Date Sat, 04 Mar 2017 18:19:09 GMT
> In FTL2 if you give something a default value (like `foo.y!0`), then you potentially unwillingly
hide mistakes in the name (there was never an "y"). But because `foo` is not just a `Map`,
we could do better. We know that `y` is not a valid name. So we could throw exception even
for `foo.y!0`.

Well, if you always throw an exception when a value is absent, I don’t
think it’s that bad, I just wouldn’t want to end up with a `null` and
an `undefined` value like in Javascript. It *could* be a little
confusing since generally nulls are not tolerated in Freemarker, but I
*think* people would learn the differences soon enough for it to not
be a big problem.

> My guess is that Ceylon goes too far there. It's very unlikely that someone is able to
comprehend something written in Ceylon, yet things like `var` or `val` would make reading
code harder for them (surely you already know what those mean). So writing `variable` and
`value` hardly have a practical value. Yes, it's consistent, I get that. But certainly many
will dislike or even be annoyed by `variable` and `value`. So to me it doesn't look like the
right balance. I prefer if the *very* common things has a sort syntax, after all, you will
very quickly learn them if you do any real work with the language.

Well, as it turns out, `value` is only two keystrokes away from `val`.
What is believed is that the time spent designing the structure of
your module, and actually writing its logic will take a much greater
amount of time than actually occasionally typing those two characters
throughout your module, so the time lost typing out “`value`” is
insignificant to the overall development time of a module. As a
consequence, you get a much more elegant‐looking code that reads much
more nicely.

And even then, `value` still comes as a much shorter way to declare
values. Instead of writing `ArrayList<Map<String, Integer>>`, you can
just write `value`.

We rarely ever use `variable` in Ceylon, so it’s okay for it to be a
little bit more verbose.

> So it's not just an assignment as in Java (where `foo = bar` does an assignment and is
also an expression with the value of `bar`). […] Ah, so the assigment is part of the `exists`
syntax...

Yeah, sorry if I didn’t make that clear. The assignment is part of the
`exists` syntax, which in turn is part of the `if` syntax.

> […] or just allow assignments as the operands of a top-level && in the case
of #if exclusively... which is kind of a hack […].

Gavin (the creator of Ceylon) said that he did consider using `&&`
instead of `,` for separating expressions in a condition list. The
problem he faced was that the assignment operator should bind closer
than the separator, however `&&` binded closer than `=`. I’m not sure
if this is a problem in Freemarker, since the `=` is part of the
`assign` directive syntax (so `&& could be made to bind more loosely
than `=` in an `if`), but *at least I* would be weirded out by `<#if
exists foo = bar && baz>`.

> […] In FTL3 I plan to replace #assign/#local/#global with #var/#val and #set.

To be honest, I think you should only have two directives. One to mean
“set” and one to mean “declare locally”. No differentiation would be
made for constants/variables. I think the perfect directive names to
use are `set` and `let`.

> Note that "behave slightly differently" in practice often just means pushing the null
requirement on your operand expression.

I don’t understand. It seems to me that most expressions will ignore
their null policy when evaluating their operands by asking for
non‐null.

For example, consider this expression: `foo.bar`. It seems to me that
`.bar` will evaluate `foo` by asking for non‐null regardless of
whether it has a `!` appended after it or not. Only if it’s asked to
*really* not return null, it will pass that restriction down the
evaluation chain, but for the other two types of evaluation, it’d
evaluate its “subordinate” expression the same way.

> Again, this is just the implementation. It's not how you explain the language rules.

If I understand correctly from the rest of your message, then this
would be explained like this:

There are two types of null: a “good null” and a “bad null”. Most
expressions (like `${}`, `.foo`, etc) can’t tolerate bad nulls but are
okay with good nulls. Some expressions (arithmetic operations, and
occasionally others) can’t tolerate either type of null; a few
expressions (`!` and `!:`) can tolerate both types of null.

Okay, then. That addresses my main concern with this approach: that it
would be hard to understand. More advanced users could investigate how
Freemarker actually implements this feature under the covers (more
atent users could be curious by seeing better error messages than they
would expect), but the average user wouldn’t have to think about it
too much.

> […] [N]amespaces accessed with colon (like in XML) are better than those accessed with
dot (as in FTL2) […].

I agree.

-----

About the whole built‐ins thing (using `?`): the more I think about
it, the more it feels to me like it shouldn’t be a thing. What is so
good about writing `foo?bar` compared to `bar(foo)`? I think the
argument would be for writing `foo?bar` instead of *`core:bar(foo)`*,
but I think it’d be a better solution overall to simply have a
different syntax for language variables (like `#uppercase("hello")`
instead of `.uppercase("hello")` or `core:uppercase("hello")` or
`"hello"?uppercase`).

I’m not completely sure about that, though. People might be too used
to their postfix function (“method”) call to be able to give it up.
For the sake of understandability, I’ll keep using `foo?bar` to mean a
similar thing to today for the rest of this message.

> The null bypassing thing addresses a quite common problem.

What I said is that we *would* have “null bypassing”, but for every
function and every parameter. I was surprised that you couldn’t store
nulls in variables (and parameters) in Freemarker 2, but that doesn’t
mean I don’t like it.

The rules would be simple:

For regular users: a function call throws if any argument is a bad
null, and returns a good null without executing the function if any
argument is a good null.

For advanced users: a function call returns null if one of its
parameters is null, but it evaluates its parameter expressions by
asking for non‐null.

> […] So let's say you are naive and write ${x!'N/A'?transformLikeThis?transformLikeThat}.
First, after some hair loss you realize that you got precedence problem there, and after you
have fixed that you end up with ${(x!'N/A')?transformLikeThis?transformLikeThat}. […]

That’s another thing that gets fixed by preferring the `#builtin`
syntax. Instead of writing
`(x!:"N/A")?transformLikeThis?transformLikeThat`, one would write
`transformLikeThat(transformLikeThis(x!:"N/A"))`.

> […] Like, x is a number or date, so you format it with the transform, but 'N/A' is
not a number or date, it's just substitute for the whole interpolated value. So you want to
put it at the end, like this: ${x?transformLikeThis?transformLikeThat!'N/A'}. […]

Well, with my approach you’d be able to do the same thing. Suppose we
keep the `?` syntax (as opposed to the `#` syntax I suggested), you’d
do it like this:

```
${x!?transformLikeThis?transformLikeThat!:"N/A"}
```

> […] We are shooting for the ${What How OrElseWhat} order for aesthetic reasons too.
[…]

Right, that’s the main concern I have with the `#` syntax I proposed:
it’s regular and nice, but it might not look as good.

> […] For the user point of view, ?upperCase etc, allows null, while ${} doesn't. If
a null touches it, it explodes. But you want FM to shut up, so you apply a `!` on the null
to tell FreeMarker not to freak out because of that null. […]

So, what you are saying is that, from the point of view of a regular
user, function call expressions whose function is a “null bypassers”
return “bad nulls” if one of its arguments is a good null, and `${}`
only handles good nulls.

> From the implementation perspective, `${}` does handle null, but it asks for a non-null
value […]. In `${maybeMissing?upperCase?trim?upperCase}` (no `!` anywhere), it's the `maybeMissing`
that will explode because it obeys the "don't dare to return null" command. […]

So, from the point of view of an advanced user, call expressions whose
function is a “null bypasser” would pass their “nullability” down to
their argument expressions.

This approach has an important flaw: the call expressions would have
to know if the function is a null bypasser. This wouldn’t be possible
if the user did something like this:

```
<#assign x = fun> <#-- or let/var/val/whatever -->
${x(thisIsNull)}
```

The call expression (the parentheses) wouldn’t be able to know if `x`
is a null bypasser or not, so it wouldn’t know if it should call
`thisIsNull` by allowing null or not.

However, if, instead, all functions were null bypassers (like I
suggested), it’d always evaluate its argument expressions by asking
for non‐nulls. (Unless they were asked to really not return nulls, in
which case they’d evaluate their argument expressions by asking them
to really not return null).

That is, in regular user terms, functions would accept good nulls (and
return a good null back while not executing their bodies), but
wouldn’t accept bad nulls.

> `!:` […] looks less cute. […]

Well, I think it looks adorable!

But seriously now, I’ve written this part of the message in a
different day than the rest (to be honest, I’ve written this message
over the span of a couple days, but that’s unimportant), and I’ve
recently had an idea: what if `:` was its own operator?

In regular user terms, it’d explode on bad nulls, but it’d accept good
nulls and return the right‐hand‐side in that case.

In advanced user terms, it’d evaluate its left‐hand‐side by asking for
non‐null, but if null is returned anyways, it’d evaluate its
right‐hand‐side.

It wouldn’t always need a `!` before it. For example, consider this:

```
${maybeNull!?foo?bar:"xxx"}
<#-- or ${#bar(#foo(maybeNull!)):"xxx"} -->

${maybeNull!.foo.bar:"xxx"}
```

Now, whenever `maybeNull` is null, `"xxx"` will be shown. But
whenever, for example, `.bar` is null, it will throw.

-----

By the way, sorry for not responding sooner.

Mime
View raw message