mavoweb / mavo

Create web applications entirely by writing HTML and CSS!
https://mavo.io
MIT License
2.85k stars 178 forks source link

Mavo functions #161

Open LeaVerou opened 7 years ago

LeaVerou commented 7 years ago

We know that naming expressions to reference them elsewhere has numerous uses. Functions are the natural next step, which allows us to parameterize these expressions that are reused.

Is this worth doing?

Authors can already define properties whose values are expressions, and this is very heavily used. This is exactly the same concept, except the expression can be parameterized and thus reused with different data.

This enables use cases like…

Syntax ideas

Design space

Decomposing the different decisions gives us the design space.

HTML element or special function?

There were some concerns on whether HTML is the best place to extend expression syntax.

I think HTML is precisely the right place for extending expression syntax, because:

Decision: HTML element.

What element to use?

Augmenting existing elements is typically an advantage when:

Neither of these is the case here. Furthermore, a custom element allows more flexibility, and attributes don't need to have mv- prefixes.

I think as a design principle, we should use elements for abstract constructs like these that have no output (e.g. I envision an element for components too).

One exception is the idea to repurpose groups for these, by being able to point to a group to turn it into a function, with one of its computed values as the output. In that case, using an existing element may make sense.

Decision: Custom <mv-function> element.

How to specify the return value?

Considerations:

Decision: TBD

How to specify arguments?

This includes things like:

Ideas so far:

Decision: TBD

Proposals


Original message

As a Mavo author, I've often felt the need to define my own expression functions, a privilege currently only available to those who know JS. For example, if you have multiple date properties, that you want to share the same format. Or, an expression that generates a custom link based on data. Or transforming data so I can render them on another template using mv-value. The possibilities are endless. If I can use expression functions, I should be able to define my own in terms of expressions and HTML.

I'm thinking of something like:

<mv-function property="myDateFormat">
    <meta property="output" content="[month(input)] [ordinal(day(input))], [year(input)]">
</mv-function>

or without custom elements:

<div mv-function="myDateFormat">
    <meta property="output" content="[month(input)] [ordinal(day(input))], [year(input)]">
</div>

and then you can call it in other expressions like [myDateFormat(date)] (where date is a date property).

Additional arguments could be input2, input3 etc. Or we could have named arguments via properties:

<div mv-function="myDateFormat">
    <meta property="date" />
    <meta property="output" content="[month(date)] [ordinal(day(date))], [year(date)]">
</div>

OR what if every group with an output property creates such a function? Then people don't need to learn ANY new syntax! Like this:

<div property="myDateFormat">
    <meta property="date" />
    <meta property="output" content="[month(date)] [ordinal(day(date))], [year(date)]">
</div>

Thoughts?

karger commented 7 years ago

So I see the appeal of being able to define your own functions. But I'm uncertain about the right syntax direction. There's a slippery slope here. Obviously once I start defining functions like yours, I'm going to discover I need conditionals. And then I may need some named variables to hold the value of common sub-expressions, etc. Before you know it you've re-implemented scheme, only with < > instead of ( ) .

There is a simple alternative---let them define a javascript function in a script tag. it immediately becomes part of the mavo namespace and callable from a mavo expression, right? It's not clear to me that the additional syntax they'll need to write (elementary) functions in js is more complicated than what they'd learn to write them in mavo.

For the closest analogy, looking at writing functions for spreadsheets: they're called macros, and it's considered a super-advanced black art (regular users never do it). And it's done in an entirely different language, VBA.

LeaVerou commented 7 years ago

Obviously once I start defining functions like yours, I'm going to discover I need conditionals. And then I may need some named variables to hold the value of common sub-expressions, etc.

Which you already have, as part of normal Mavo syntax! That's the beauty of it.

There is a simple alternative---let them define a javascript function in a script tag.

That means they need to know how to write JS, which is not the same as utilizing Mavo expressions. E.g. the expression for the date format above would become in JS:

function myDateFormat(date) {
return Mavo.Functions.month(date) + " " + Mavo.Functions.ordinal(Mavo.Functions.day(date)) + ", "
 + Mavo.Functions.year(date);
}
LeaVerou commented 7 years ago

Oops, wrong issue number

LeaVerou commented 7 years ago

For the closest analogy, looking at writing functions for spreadsheets: they're called macros, and it's considered a super-advanced black art (regular users never do it). And it's done in an entirely different language, VBA.

Well, some people think it should be easier: https://dl.acm.org/citation.cfm?id=1370867 :)

mycaule commented 6 years ago

I like the analogy with Excel spreadsheets. We all know that Excel is a great prototyping tool for small applications. It would be great if Mavo could allow users to write dashboard applications with "spreadsheet functions".

I think Mavo.Functions should include or let users include other simple utility libraries such as https://lodash.com/, https://vocajs.com/, http://momentjs.com.

For the design, have a look at lowdb. It is an interesting example, it is a file-based DB that exposes all the functions that are in lodash.

From the user point of view: "Oh, I know how to solve that problem with arrays. It is the same if I want to do it with my database".

caraya commented 6 years ago

For the design, have a look at lowdb. It is an interesting example, it is a file-based DB that exposes all the functions that are in lodash.

We need to keep in mind who Mavo's target audience is and what kind of apps we want them to build with this tool. You're assuming that people who use Mavo would also use or be comfortable using a database or any other analogy to more complex programming and I think that's not the point.

It is a very slippery slope in terms of the functionality that you require these functions to have. I'd much rather look at how can we surface some existing functionality that does the same thing than libraries or if we can improve what's already there.

If the user really needs lodash, voca or the full moment llibrary then I don't see a problem with them dropping to Javascript to use them. It goes beyond what someone in the target audience for Mavo would normally do when building an app with Mavo.

LeaVerou commented 6 years ago

@caraya is making excellent points here. Also, whoever knows JS and wants to use lodash can just include them and then do $.extend(Mavo.Functions, _) and done. Similar for the rest.

Please keep in mind that this thread is about how Mavo authors (who do not necessarily know JS) can define their own functions and whether this is useful. Can we stay on that topic, please? :)

For other feature suggestions, you can always open a new issue.

mycaule commented 6 years ago

In my opinion. It is easier to add a JS function if the Mavo user knows the concept of a function, than to hack HTML tags. You already require the user to know HTML and CSS, which are already "hard".

If you look at the Mavo.Functions object in functions.js you already have utility functions that are covered with lodash, voca and moment.

Questions arise when looking at the code:

As a project maintainer, you should just include a string, number and date utility library and wrap those calls instead of reinventing the wheel. In the mavoscript documentation, you could just say "advanced user who need more functions can use lodash calls" like lowdb library does in their documentation.

If you let the user plug a popular library (you don't even need to tell them to it is lodashand to learn it) you are sure to cover most usecases, similar to Excel having a large panel of utility functions that don't require you to write VBA to solve common problems. The libraries I mentioned also don't require you to be proficient in programming if you want to use one of their methods. Check also formula.js which is a JS implementation of all Excel function.

In Excel, you don't need to write functions, just do plenty of cells with intermediate values, which you can already do using <input property="..."> as variables. You can also use <input type="hidden" property="..."> to hide intermediate values.

So for the initial problem,

    <time property="birthday" datetime="2014-06-01"></time>
    <input type="hidden" property="formated_date" value="[month(birthday)] [ordinal(day(birthday))], [year(birthday)]">

Basically when building Mavo, you are investigating the same design problems that a spreadsheet application has. Knowing this, retro-engineering the design of Google Sheets could be a great idea.

It is not off topic, you need to consider all the facts.

PS. I am trying to make a clone of some functionalities of Bloomberg Watchlist app using your framework. It is an spreadsheet-style application that lets you track stocks you invested in. You can have a look in this repo: https://github.com/mycaule/portfolio

caraya commented 6 years ago

As a project maintainer, you should just include a string, number and date utility library and wrap those calls instead of reinventing the wheel. In the mavoscript documentation, you could just say "advanced user who need more functions can use lodash calls" like lowdb library does in their documentation.

Basic functionality is already available and documented:

Dates Text/Strings

All Javascript functions are available unprefixed to Mavo

I think the most important part is that you, as the user who needs them can add them to your application... If we follow your logic then Lea would have to add whatever library a user wants because it's easier for them to use; I personally prefer Backbone rather than Underscore. Where do maintainers draw the line?

If I'm reading Mavoscript versus Javascript correctly, Mavoscript will jump to Javascript for advanced expressions. The one doubt I have is whether the automatic Javascript parsing includes external libraries or only the functionality built into the browser's JS engine.

As far as Mavo's design principles, it has more to do with easing entry into web design and development, not a programming tool. The spreadsheet analogy only goes so far in explaining the purpose of Mavo. I'll defer to Lea (who create Mavo to being with) to speak more about his.

LeaVerou commented 6 years ago

@mycaule There are a number of reasons why the default set of functions does not come from a library:

Are they well tested and optimal?

This is part of the testsuite: https://test.mavo.io/functions.html Of course more tests would be better, but that applies to everything.

Are they documented?

https://mavo.io/docs/mavoscript Of course more docs would be better, but that applies to everything.

As a project maintainer, you should just include a string, number and date utility library and wrap those calls instead of reinventing the wheel.

Is that an RFC 2119 “should”?

"advanced user who need more functions can use lodash calls"

That’s the thing, it shouldn't just be advanced users that can use expressions. In our user study we observed that most HTML authors with no programming experience could use simple functions. This is a major factor in designing Mavo’s functions.

You can also use to hide intermediate values.

Or <meta property="..." content="..."> which is the recommended way. But hidden inputs work too.

PS. I am trying to make a clone of some functionalities of Bloomberg Watchlist app using your framework. It is an spreadsheet-style application that lets you track stocks you invested in. You can have a look in this repo: mycaule/portfolio

Exciting! Would love to see the final result!

Bottom line is, I mentioned above that you can use any utility library you want in Mavo expressions, all you have to do is include it. You can even use its functions with the same syntax as native Mavo functions (i.e. no _.) by running one line of script to add them onto the Mavo.Functions object. For anyone aware of moment.js or lodash, a line of JS should be no problem. Therefore, I’m really not sure what's the issue: Is it that what I said above is unclear? Is it that you want this mentioned in the docs? Is it that you want everyone to use lodash? Is it that you don't like the default Mavo functions and you want them to disappear? What is it?

And I'm sorry, but it's still off-topic. The default set of functions is orthogonal to whether Mavo authors can create their own functions and how. This has nothing to do with which utility library we use, even if we used lodash or whatever, people might still want to re-use their expressions, just like people want to do this in spreadsheets (just google "custom excel functions" for an idea of how many).

LeaVerou commented 6 years ago

Most of what @caraya is saying is correct (thank you!).

If I'm reading Mavoscript versus Javascript correctly, Mavoscript will jump to Javascript for advanced expressions. The one doubt I have is whether the automatic Javascript parsing includes external libraries or only the functionality built into the browser's JS engine.

It includes everything that would be available to normal JS. It don't even need to jump to JS for that, if the function exists it will be used. It's syntax-level stuff that it will jump to JS for, e.g. array.map(x => x + 1), because MavoScipt doesn't support function definitions.

As far as Mavo's design principles, it has more to do with easing entry into web design and development, not a programming tool.

"Easing entry into" implies that we see Mavo as a temporary, educational solution until people are ready to move into "real coding". That’s not the case, though of course it could work that way and it does for many people. Our vision is that Mavo (+ HTML & CSS obvs) eventually will be sufficient for creating most data-driven web applications that people need to make, and learning JS and backend stuff will mainly be needed for specialized or high performance tasks, just like the other abstractions that we're used to today. E.g. we don't see C++ as a stepping stone to Assembly and we don't see CSS as a stepping stone to WebGL. Of course, we're still quite far from that vision, but it does guide our decisions :)

mycaule commented 6 years ago

These are just suggestions by the way. Your do your product management as you like.

I also that one with Mavo (spreadsheet type application with visualisation). https://github.com/mycaule/can-we-row

Had to write server side code, but realized in the end I could package all the HTTP calls client side with parcel bundler like I did in the portfolio app.

Some observations with those two similar apps:

LeaVerou commented 6 years ago

@mycaule I would love to look into these problems, but you will need to open new issues for them.

LeaVerou commented 5 years ago

Revisiting this, since it becomes even more needed with data formatting I think this thread has overcomplicated it, which is why the feature stalled. I literally cannot count the number of times I've needed this in the simplest way, and I had to escape to JS, which is harder than it looks because the values Mavo passes around are not the primitives one would expect, but complex objects to preserve references. Mavo expressions hide this complexity automatically, but once you're in Javascriptland, all of it is exposed.

We should add something very simple, like:

<div mv-function="functionName" mv-parameter-1="arg">[doStuff(arg, propertyName)]</div>

Any element can be used. E.g. <meta> might make more sense:

<meta mv-function="functionName" mv-parameter-1="arg" content="[doStuff(arg, propertyName)]">

Anything that requires more complex stuff can be done via JS.

Things to be decided:

Thoughts @karger @DmitrySharabin?

karger commented 5 years ago

I want to be sure I understand what you are proposing. This is a way to define functions in mavo? So your example defines a function named functionName that takes a single argument "arg", and is evaluated by plugging arg into dostuff?

It seems a little odd that we are using html/angle bracket syntax to define functions that get used in js/parens syntax. Would it be more self consistent to have some kind of "define" operator that gets used in expression evaluation contexts? e.g. [define(functionName(arg1), expression-on-arg1)]

karger commented 5 years ago

I recommend reading this paper for insight. They are tackling a parallel problem, of letting users define new functions in spreadsheets.

Here's one question from thinking about what they did. Your syntax above lets you define functions with one-line bodies, but what if you have a much more complicated function that e.g. needs to define some intermediate values that are then combined into the final result? It would be nice if we could e.g. use mv-value elements inside the function body (which therefore would need to incorporate html, not just a mavo expression).

so for example a syntax like this:

<div mv-function="functionName" mv-arg="arg1" mv-value="ret">
    <meta property="computed" mv-value="2*arg1">
    <meta property="ret" mv-value="1+computed">
</div>

The intent of this syntax is to say "the function, when invoked, binds the local arg1 to the first named parameter of the function, then returns as its result the value of the "ret" property."

But this approach still mingles html with what fundamentally seems like an expression-language issue. I could still see an argument for just having a javscript-like representation---perhaps one that elides the difference between the primitives the author expects and mavo's "complex objects to preserve references" that you mention above.

karger commented 5 years ago

I'll also bring in the somewhat related issue of templates. Just like we write functions to avoid repeating ourselves, sometimes we want to display the same kind of information the same way in different places on a page. Mavo makes this easy in some cases---e.g., we write the template once for a collection which then gets replicated for each item. But what if I want to display same item same way in multiple locations. Wouldn't it be nice if I could define the presentation once and use it both places? We already do this for editing templates with mv-edit; how about for viewing as well with e.g. an mv-template attribute? Which would say to use the same data model for the template as well as the same presentation? This would make it much easier to e.g. present hierarchical structures such as a family tree (or a threaded comment list?).

DmitrySharabin commented 5 years ago

I am sorry for being so slow in response (I was a bit busy preparing my first talk about Mavo).

I read all the thread carefully, and here are some of my thoughts:

We can also let users don't specify the names of the arguments. In that case, we interpret arguments passed to a function in the order they were defined. So we consider sum(10, 15) and sum(first: 10, second: 15) having the same meaning.

I think that would let us solve the problem Lea mentioned:

...if one has many parameters and wants to change one's position, they would need to rename many attributes. It would be better if there was a syntax to specify parameters where one only had to make one edit in that case.

  1. If we define a function with the help of a non-empty element (like <div>) do we really need to use square brackets as if it was an expression? What if we omit them as we do so in the mv-value and mv-action attributes? I think that won't break our logic but will let us use square brackets in a function body if we need it. So we would have a syntax which is a bit simpler:
    <div mv-function="functionName" ...>
    doStuff
    </div>
    ...
  2. I agree that it would be nice if we could use intermediate values in a function body, but I'm a bit confused with the amount of HTML elements I need to use to write a rather simple function. What if we could interpret a function body as a paragraph text. To be more precise:
  1. I also agree that we need to define somehow the returned value. I think we could use a syntax similar to that we use to define parameters: mv-return-retName.

Combining points 2 and 3 we can rewrite David's example like so:

<div mv-function="functionName" mv-parameter-arg1 mv-return-ret>
    computed = 2 * arg1
    ret = 1 + computed
</div>

I'd like also mention that if we didn't define the return value explicitly, the last statement of the function body (the last line) could be interpreted as the function value:

<div mv-function="functionName" mv-parameter-arg1>
    computed = 2 * arg1
    1 + computed
</div>

Defining a function's return value (variable) explicitly would let us define functions that return more than one value (like in Swift). For example,

<div mv-function="stats" mv-parameter-data mv-return-avg mv-return-minimum mv-return-maximum>
    avg = average(data)
    minimum = min(data)
    maximum = max(data)
</div>

We could get these values using, e.g., dot notation: [stats(list(1, 3, 12, -8)).avg].

I'm not really sure about use cases. Just my thoughts. :)

Any identifiers not named via mv-parameter-* are evaluated as properties. But in which group? The group the function is defined in or where it's used?

Do you think we need to define impure functions? I think they are a bit difficult to maintain for inexperienced programmers. No?

joyously commented 5 years ago

If you implemented it as stated, would it be possible to document it within a Mavo? In other words, can you turn it off so you can show examples, inside a Mavo?

DmitrySharabin commented 5 years ago

There is one more thing I was thinking about. Wouldn't it make sense to recommend users to define functions, which body is built out of more than one line, by using the <template> element? Of course, if we decide to implement this feature.

LeaVerou commented 5 years ago

@DmitrySharabin Interesting. Why the <template> element? What's the reasoning?

DmitrySharabin commented 5 years ago

Well, the main reason (as I see it, I might be wrong) is the analogy with the <meta> element: 1) its content is not rendered by the browser (by default) 2) inside the <template> element we can define functions and not adding new semantics to the existing elements (if we define a function inside one of them instead) 3) if a function is written with the help of more than one line, by putting it inside a non-empty element (between the opening and the closing tags) would increase its readability.

LeaVerou commented 5 years ago

Why an analogy with the <meta> element and not a <meta> element itself? 😀

DmitrySharabin commented 5 years ago

The <meta> element is perfectly fine for most cases. I agree. 😅 I was just thinking about some complex functions David mentioned. Like this one:

<div mv-function="functionName" mv-parameter-arg1 mv-return-ret>
    computed = 2 * arg1
    ret = 1 + computed
</div>

Is there a way to define these kinds of functions in the <meta> element? We need a way to separate statements that define the function body, no?

DmitrySharabin commented 10 months ago

Thinking of another issue we have, I came up with an idea of another way of defining custom functions—via a special function.

Microsoft and Google added the LAMBDA() function to their function language for quite some time. So, we might follow their way.

Suppose we added to Mavo the lambda() (we might want to name it some other way) function, which has the following signature: lambda(par1, par2, ..., parN, computations), where computations is an expression depending on par1, par2, ..., parN. All parameters, except computations, are optional. The result is a function that expects N arguments to perform computations and is ready to be invoked in other expressions.

In that case, we might have something like this:

<meta property="my_function" content="[lambda(x, y, x + y)]" />
...
[my_function(1, 2)] <!-- 3 -->
[my_function(list(1, 2, 3), list(4, 5, 6))] <!-- list(5, 7, 9) -->

The lambda() function might also be used inline, like so: [lambda(x, y, x + y)(1, 2)]. It's not the most readable expression, though.

In any case, the lambda() function is not the most straightforward concept for authors to absorb. The same is valid with spreadsheets as well.

Another issue with this approach is that we might not use the property attribute as the authors used to. I'm unsure if something like “attribute overloading” applies here. 🤔 Or should we define functions using the earlier proposed mv-function attribute?

LeaVerou commented 10 months ago

@DmitrySharabin Nice thinking, how other products solve this problem is definitely very useful.

However, I’m not a fan of the ergonomics of this syntax. Remember "make common things easy and complex things possible"? This seems to do neither.

But thinking about this again after all these years, I think I have a…

New proposal

A <mv-function> component that works as follows:

Examples

A bit formulaic due to lack of time — also we already have a hypot() function as we adopt all Math functions, but let's ignore that briefly).

In some I’m assuming dot notation is implemented (see #1009).

Compact syntax

Without naming arguments:

<mv-function name="hypot" content="sqrt(pow($args[0], 2) + pow($args[1], 2))"></mv-function>

Naming arguments:

<mv-function name="hypot" params="a: 1, b: a" content="sqrt(pow(a, 2) + pow(b, 2))"></mv-function>

Specifying the body in the contents:

<mv-function name="display_repo_name" params="repo">[
    if(repo.startsWith("leaverou/"), repo.from("leaverou/"), repo)
]</mv-function>

Variable number of arguments

Beyond dot notation, this also uses a map() function that works with a special property for the current value (see #1010):

<mv-function name="hypot" content="sqrt($args.map(pow($value, 2)).sum())"></mv-function>

Extended param syntax

<mv-function name="hypot" content="sqrt(pow(a, 2) + pow(b, 2))">
    <mv-param name="a" required type="number"></mv-param>
    <mv-param name="b" default="1" type="number"></mv-param>
</mv-function>

Combined with contents:

<mv-function name="display_repo_name" params="repo">
    <mv-param name="a" required type="number"></mv-param>[
    if(repo.startsWith("leaverou/"), repo.from("leaverou/"), repo)
]</mv-function>

Questions

joyously commented 10 months ago

I'm not an expert on JS or Mavo, but I'm a programmer, and I can't tell what your examples do. The square brackets are confusing, the elements that start with mv- look like custom HTML elements instead of part of Mavo, and I can't see whether you are defining a function or invoking it inline. Also, how would one specify a string literal in an attribute? This all seems way too complicated. I would rather just use JS if I have need of a function. Perhaps all that is needed is an interface to the Mavo object or the this pointer? (sort of like jQuery)

LeaVerou commented 10 months ago

@joyously

I'm not an expert on JS or Mavo, but I'm a programmer, and I can't tell what your examples do. I would rather just use JS if I have need of a function. Perhaps all that is needed is an interface to the Mavo object or the this pointer? (sort of like jQuery)

I think there may be a misconception here. While we generally try to cater to both, Mavo is primarily a language for people who don't know JS or other imperative programming languages, and novices are above programmers in the priority of constituencies, since there are so many other tools that cater to programmers.

And it's not uncommon to see that syntax that works for novices confuses programmers and vice versa. As an example, in the first Mavo study, novices found the idea of adding mv-multiple on an element to make it a collection natural, while programmers were trying to figure out how to iterate.

The square brackets are confusing, the elements that start with mv- look like custom HTML elements instead of part of Mavo, Also, how would one specify a string literal in an attribute?

These are syntactic constructs that apply far more broadly to Mavo.

and I can't see whether you are defining a function or invoking it inline. This all seems way too complicated.

I would love to see some non-complicated syntax that makes it the distinction more obvious!

joyously commented 10 months ago

novices are above programmers in the priority of constituencies

Yes, I understand that, but I think the novice would not make any sense at all out of those examples. I barely could, even with you explaining it. Reinventing the wheel...why?

Brackets are how you define an expression among content

That was not obvious. It was confusing.

You specify strings in attribute expressions all the time

Yes, but you are suggesting to put code into an attribute, which is delimited by quotes. What if the code needs quotes for a string? I think the novice will have trouble and not know to put the code as content (and also put square brackets around it).

mv- is a prefix used all over Mavo (on classes, attributes, etc.)

So far, it seemed that it was only attributes, not elements, which makes it much easier to work with.

I would love to see some non-complicated syntax

So would I. I don't really see where this is needed, so a better use case would be nice, so I can understand if you define a function where you use it or in a separate file you reference, or what. If it's inline, why make a function? (maybe reread older previous comments above)

LeaVerou commented 10 months ago

Yes, I understand that, but I think the novice would not make any sense at all out of those examples. I think the novice will have trouble and not know to put the code as content (and also put square brackets around it).

What novices find confusing is something we determine via user testing, not speculation. The output-value-as-content is mainly useful when concatenating one or more expressions with regular text (i.e. the brackets would not encompass the whole content), for expressions like those in the examples, I think content is a better choice.

I barely could, even with you explaining it.

As I explained above, it doesn't follow that if a programmer cannot understand something, a novice also won't. Programmers are not the target group for this feature, as they can define JS functions with only a little more friction, and more power.

Yes, but you are suggesting to put code into an attribute, which is delimited by quotes. What if the code needs quotes for a string?

Again, this is exactly how Mavo works already. There are directive attributes (like mv-value) whose value is always interpreted as an expression, and regular attributes, which can contain expressions in []. In both of these cases, including strings in these expressions is common, and is typically done by inverting the type of quote. Contrived example that showcases both:

<span mv-value="sum(numbers)" style="color: [if(foo, 'red', 'green')]">0</span>

So far, it seemed that it was only attributes, not elements, which makes it much easier to work with.

It's also used for custom properties (and JS events). It has always been the plan to use it on custom elements as well, when it made sense for the use case. I’m really not sure what makes <mv-function> harder than <div mv-function> except that it's a new pattern (that we plan to use for other things too).

So would I. I don't really see where this is needed, so a better use case would be nice, so I can understand if you define a function where you use it or in a separate file you reference, or what. If it's inline, why make a function?

I’ve started a list in the first post, and we can expand it as we stumble on more use cases. But basically, it's the same concept as a property whose value is an expression; it just allows you to parameterize the expression. For example, one of the most common use cases is defining custom formats to output data, e.g. date formats.

(maybe reread older previous comments above)

I actually spent some time today to re-read the whole thread. Here’s some high level comments to things that have been said:

DmitrySharabin commented 10 months ago

@LeaVerou I really like your proposal! Thanks to its decalrativeness (if I can say so), it feels close and similar to HTML and SVG. From my perspective, people who write HTML, SVG, and also MathML will feel the same.

Can params default to other params (in that case, authors might not need to specify its type since it can be inferred)? Like so:

<mv-function name="hypot" content="sqrt(pow(a, 2) + pow(b, 2))">
    <mv-param name="a" required type="number"></mv-param>
    <mv-param name="b" default="a"></mv-param>
</mv-function>
<mv-function name="hypot" params="a: 1, b: a" content="sqrt(pow(a, 2) + pow(b, 2))"></mv-function>

Do I get it correctly that you are showcasing it here?

Should we introduce any special handling rules for whitespace? (see the "contents in the body" examples, without putting the braces like that we'd also get whitespace as the result)

It feels like we should. At least we might trim them and replace multiple whitespace occurrences with one as HTML does. We might also provide an attribute to preserve whitespace, possibly similar to the one we have in CSS.

karger commented 10 months ago

I have to say that in all of my mavo writing I can't recall a time when I wanted to define a function to get a result. Instead I would just write a computation through a sequence of properties each depending on the ones that came before. This allowed my to express complex multi-stage computations; it did not allow for code reuse but I don't recall needing that. The one place where I have had complicated messy formulas that I couldn't" make manageable this way was complex compound actions*, which can't be broken out into individual property "steps" the same way---that might be worth thinking about.

It seems that you are thinking about using html for function definition but are still thinking about formulas for function invocation. Note that we could use html for invocation as well. If hypot is defined with params a and b, then

<span property="distance" mv-function-call="hypot">
   <span property="a" mv-value="3"></span>
   <meta property="b" mv-value="4">
</span>

would result in the "distance" property having value 5. mv-function-call is not the right name of course but conveys the intent. As you say above, html is the "UI" of mavo so it's nice to have the function call in the html instead of hiding in formulas whose params cannot be inspected in the dev console. One downside is that this kind of inverts the current mavo concept, where the value of a parent element renders onto its children; here instead the value of the parent element is computed from its children. Will that be confusing?

karger commented 10 months ago

I wonder if we can reuse more syntax. Conceptually, we often think of implementing a function call as substituting the body of the function for the invocation, replacing params with args. In mavo we already have something close, which is the rendering of an object onto a fragment of html. If you are using html to define the function you can just use existing syntax to define the body such that rendering the argument object onto the body does the right thing. Above you defined a function this way:

<mv-function name="hypot" content="sqrt(pow(a, 2) + pow(b, 2))">
    <mv-param name="a" required type="number"></mv-param>
    <mv-param name="b" default="1" type="number"></mv-param>
</mv-function>

Instead, how about

<template mv-function="hypot" mv-output-property="c">
    <meta property="a" type="number"></mv-param>
    <meta property="b" mv-default="1" type="number">
        <span property="c" mv-value="sqrt(pow(a, 2) + pow(b, 2))"></span>
</template>

This works the natural way when you call with an argument object (named parameters). If you call with an argument list, you can use the mavo heuristics for rendering a list onto an object.

I think an advantage of this approach is that it lets a person "develop" a function as a normal mavo fragment, then turn it into a function by just wrapping it unchanged it inside an mv-function element.

Getting fancy, if I wanted to "trace" this function, I could use an mv-trace attribute to indicate which (only one)) invocation of the function (from html, see previous comment) is the one I want to trace, and mavo could instantiate the html of the function with the (current) arguments being passed to it from the traced invocation:

<span property="distance" mv-function-call="hypot" mv-trace>
   <span property="a" mv-value="3"></span>
   <meta property="b" mv-value="4">
</span>

Small problem; if you invoke the function on (every) mv-list-item I don't know how you decide which one to trace. Arbitrary? Last invoked? Because often you're debugging with only one element in the list?

LeaVerou commented 10 months ago

@karger

I have to say that in all of my mavo writing I can't recall a time when I wanted to define a function to get a result.

This allowed my to express complex multi-stage computations; it did not allow for code reuse but I don't recall needing that.

Did you ever write a JS function for Mavo? Pretty sure I recall you needing that.

Instead I would just write a computation through a sequence of properties each depending on the ones that came before.

Yes, this is not about abstraction, it's about reuse.

The one place where I have had complicated messy formulas that I *couldn't" make manageable this way was complex compound actions, which can't be broken out into individual property "steps" the same way---that might be worth thinking about.

The intent was for <mv-function> to support an attribute that says "this is an action", but perhaps it should be a different construct (<mv-action>?)

It seems that you are thinking about using html for function definition but are still thinking about formulas for function invocation. Note that we could use html for invocation as well. If hypot is defined with params a and b, then

<span property="distance" mv-function-call="hypot">
   <span property="a" mv-value="3"></span>
   <meta property="b" mv-value="4">
</span>

would result in the "distance" property having value 5. mv-function-call is not the right name of course but conveys the intent.

As I wrote, relegating things to the UI typically lowers the floor but also the ceiling. In this case, using this for function calls has a clear power tradeoff: You now cannot use functions in attributes, not to mention poor efficiency (both for writing and reading code). Pure functions are generally reasonably well understood — functions defining functions via function calls not so much. Debugging should follow syntax, not guide it — good dev tools should make it possible to inspect parameters for all function calls.

karger commented 10 months ago

Did you ever write a JS function for Mavo? Pretty sure I recall you needing that.

Definitely, but I wrote them to do things that are not possible in mavo, not to provide reusability in mavo.

Yes, this is not about abstraction, it's about reuse.

I think in my experience there was relatively little need for reuse beyond the reuse that already happens thanks to mv-list. ie I haven't needed a function to use in two different expressions.

karger commented 10 months ago
<span property="distance" mv-function-call="hypot">
   <span property="a" mv-value="3"></span>
   <meta property="b" mv-value="4">
</span>

would result in the "distance" property having value 5. mv-function-call is not the right name of course but conveys the intent.

As I wrote, relegating things to the UI typically lowers the floor but also the ceiling. In this case, using this for function calls has a clear power tradeoff: You now cannot use functions in attributes, not to mention poor efficiency (both for writing and reading code).

This can coexist with calling functions the traditional way for people who understand that

Pure functions are generally reasonably well understood — functions defining functions via function calls not so much.

this isn't a special function defining function call. it's just saying to invoke the hypot function

Debugging should follow syntax, not guide it — good dev tools should make it possible to inspect parameters for all function calls.

but this way people don't need to learn new dev tools

LeaVerou commented 10 months ago

Did you ever write a JS function for Mavo? Pretty sure I recall you needing that.

Definitely, but I wrote them to do things that are not possible in mavo, not to provide reusability in mavo.

Yes, this is not about abstraction, it's about reuse.

I think in my experience there was relatively little need for reuse beyond the reuse that already happens thanks to mv-list. ie I haven't needed a function to use in two different expressions.

Ok, thanks for the additional data point. Other people have needed them, including me and Dmitry. It's definitely more of a power user feature, and not top priority, but the use cases are there (as evidenced by the fact that spreadsheets are adding syntax for them).

This can coexist with calling functions the traditional way for people who understand that

Sure, but it's unclear to me that the value-add is worth the additional complexity.

Pure functions are generally reasonably well understood — functions defining functions via function calls not so much.

this isn't a special function defining function call. it's just saying to invoke the hypot function

I know. I was explaining why I think it's a good idea to specify the definition in HTML, but not the call.

Debugging should follow syntax, not guide it — good dev tools should make it possible to inspect parameters for all function calls.

but this way people don't need to learn new dev tools

Languages are far harder to change than dev tools or UIs around the language, and not having to learn dev tools is a non-goal. No syntax you can design will ever beat good dev tools, so the earlier people learn to inspect their apps, the better.

In fact, not designing syntax around debugging is an implicit design principle in the CSS WG, and come to think of it, it I think I may propose it as a more general web platform design principle.

DmitrySharabin commented 10 months ago

I was wondering whether it is a good idea to introduce variables to hold intermediate values, e.g., for code reuse and improve the readability of complex functions.

Something like this (formula-first):

<mv-function name="hypot" content="sqrt(c)">
    <mv-param name="a" required type="number"></mv-param>
    <mv-param name="b" default="a"></mv-param>
    <mv-var name="c" type="number" value="pow(a, 2) + pow(b, 2)"></mv-var>
</mv-function>

OR (content-first)

<mv-function name="hypot" content="sqrt(c)">
    <mv-param name="a" required type="number"></mv-param>
    <mv-param name="b" default="a"></mv-param>
    <mv-var name="c" type="number">[pow(a, 2) + pow(b, 2)]</mv-var>
</mv-function>
DmitrySharabin commented 10 months ago

Some other thoughts:

LeaVerou commented 10 months ago

@karger

I wonder if we can reuse more syntax. [...]

Yup, that's one of the options if you read the thread from the start. Even repurposing groups has been proposed as a potential idea. Though I see I have not added these ideas to the summary in the start, and I cannot remember what the issue was. Possibly that this way it's harder to make them side effect free?

@DmitrySharabin

I was wondering whether it is a good idea to introduce variables to hold intermediate values, e.g., for code reuse and improve the readability of complex functions.

Oh absolutely. But I think it makes sense to have these as regular properties.

  • Would it be more natural for novices to number arguments starting from 1: args[1] — the first argument instead of args[0]?

Given lists are already 0-indexed, I think internal consistency should be the priority.

  • Will it be also possible to call arguments like so: args.1, args.2, etc.?

I don't think that's parse-able, but if it is it would be worth doing for all lists.

Worth exploring.

DmitrySharabin commented 10 months ago

I was wondering whether it is a good idea to introduce variables to hold intermediate values, e.g., for code reuse and improve the readability of complex functions.

Oh absolutely. But I think it makes sense to have these as regular properties.

Do we expect those properties to be scoped to the function and inaccessible in other app parts if the author declares them inside the function definition?

Are we planning to let authors refer to properties defined outside the function (when defining a function)? Or do we suppose authors would be defining only pure functions?

karger commented 9 months ago

I'm nervous about creating a full programming language inside Mavo. It's not clear what the benefit is over giving them a way to incrementally learn and add js.

LeaVerou commented 9 months ago

Mavo is a full programming language — the question is what is part of the formula language, and what is part of HTML. The lack of parameterizable abstractions is a pretty significant lacking in Mavo, especially when making nontrivial applications.

JS is designed with different goals in mind. I don't think there's something inherent in creating abstractions that should make them available only to the privileged few who are technical enough to be able to learn JS. It's all about the ease of use to power curve and avoiding cliffs. Right now, everything that requires JS is a cliff; I argue that functions are common enough where that is not ok.

Another way to look at it is that functions serve the same purpose as computed properties, but allow parameterization.

Anyhow, the discussion is likely moot, since there is a ton of much higher priority things that would be worked on before this.

karger commented 9 months ago

Rather than "moot" I would say that the discussion is "academic", and since I'm an academic....

I agree about wanting to avoid cliffs. But it isn't clear to me that introducing complicated concepts into a PL that has tried to stay simple is the right way to avoid the clifff---I think it's the hard concepts, rather than the syntax, that create a js cliff. As a contrast, what consider the possibility of introducing a simplified subset of js---that we parse ourselves for consistency and clarity---that provides the functionality we consider necessary. Instead of boxing them in to our mavo language, it becomes a starting point for eventually lifting themselves to full js expertise.

fatherofinvention commented 3 months ago

It's too bad this never took off. Lots of great ideas in here and I think this is an important feature to add if/when the team ever finds the time. Mavo rules.