Stabel

View Ticket
Login
2021-06-02
10:03 Closed ticket [57c2233d50]: Alpha 2: Module definitions plus 4 other changes artifact: 98ad5befe0 user: robin.hansen
10:02
Add support for modules. Fixes [57c2233d50] and [abaac5803b] check-in: e155197c9a user: robin.hansen tags: trunk
2021-01-16
11:00 Ticket [57c2233d50] Alpha 2: Module definitions status still Open with 4 other changes artifact: e33019398f user: robin.hansen
2021-01-09
13:27 New ticket [57c2233d50]. artifact: 1c26f51190 user: robin.hansen

Ticket UUID: 57c2233d5024ce2fafba288e708e9d9c2d887794
Title: Alpha 2: Module definitions
Status: Closed Type: Language_Proposal
Severity: Important System: Compiler
Resolution: Fixed Modified: 2021-06-02 10:03:38
User Comments:
robin.hansen added on 2021-01-09 13:27:35:

The following document outlines the language proposal process: Language Proposals

Defining modules

To enable any practical program written in Play, it's vital that we support splitting code up in different files and still treat such code as part of the same program. This proposal looks at the language syntax for defining modules. Another proposal deals with resolving modules, that is locating a module on disk.

Module definition

Module definitons are _optional_. By default the canonical module name is the same as its disk location. For instance, if there is a package foo, and a file is located at {foo_path}/src/controllers/root.play, then the canonical name would be controllers/root.

All modules, whether in the same package or in a third-party package, can be referenced using its canonical name. As such, import statements aren't necessary. As an example, if your package depends on a hypothetical html library, accessing its div function would be a simple matter of writing html/div.

That being said, it's quite likely that developers would want to write a module definition anyway to change how references are made or to write documentation for a given module. In those cases, the .play file would have to start with a module definiton, and it would look something like this:

defmodule:
doc: """
A module containing html node constructors
"""
alias: impl html/internal/node
exposing: html/interal/attributes
:

# Example function def: div : impl/div

There are several things to note with this example:

  • You don't have to write the module name. It will always be the same as the physical file location on disk anyway, so why require the repetition?
  • The doc: annotation will be implemented in the future when here-strings have been implemented, and will work on all definitions, not just module definitions.
  • alias annotations allows creating a, usually shorter, alternative name for a canonical module. It contains two parts, seperated by whitespace. The first part is the alias itself, while the second is the canonical module that is referenced by the alias.
  • exposing gives access to a given module's definitions as if they were defined in this module. You can insert the names of several definitions after the canonical module name if you don't want to expose _all_ of the module's definitions.
  • The final colon marks the end of the module definiton.

Named, unresolvable modules

By default, all definitions within a module are exposed. That is, they are accessible to outside modules. To make a definiton not accessible, you can add it to an unresolvable module:

defmodule:
doc: "This module's definitons are accessible to other modules"
:

# Example function def: div : impl/div

defmodule: impl doc: "This module can only be referenced by the above module" exposing: html/internal/node node :

def: div : node "div"

Since modules are expected to share its name with its on-disk location, a module-within-a-module cannot be resolved. As such, all definitions in such modules are inaccessible.

There are a couple of benefits with this approach:

  • Less typing to define which definitions are accessible and which aren't.
  • Re-structuring your code is easier as you can prototype the new module layout within a single file.

You can have as many unresolvable modules as you want within a single file. The only requirement is that the top module definiton must match the file's path, or be unnamed.

To reference a definition in the parent module, you start a name with the / character. So if the impl/div in the example above would like to create recursive loop, it could call /div.

The problem with placing things in unresolvable modules is that you break locality. Sometimes it can increase readability to have several related functions in close proximity, but only have one of them be accessible outside of the current module. For that reason I'm willing to entertain adding a module: private annotation to add a definition to an unresolvable module (named private, in this case) without actually having it located elsewhere in the file. This would likely not be implemented until after I've had some experience with unresolvable modules. It might turn out that breaking locality isn't so bad in practice.

Entry points

A program's entry point (meaning the function called when a program starts up) doesn't require any special syntax. Any function can be used as an entry point as long as it matches a specific type signature suited for a given platform (will be different across repl, browser, terminal, etc.). The function that should serve as the entry point for a program needs to be passed as an argument to the compiler.


robin.hansen added on 2021-01-16 11:00:06:

Based on feedback, the proposal will have the following changes:

Limited expose-by-default behaviour

Several have noted that to expose all functions in a module, and after some time I've come to agree with that view point. It is quite likely that helper functions will be more common in Play than other languages because of the lack of local variables, and such functions have no use to be part of an API.

At the same time, I still feel that being able to just create a file with minimum module boilerplate enabled more rapid prototyping, which I feel is important in the early stages of a project.

The proposal will therefore change so that modules can now define its exposed functions. The keyword for exposing functions will be "exposing:" and it will contain a whitespace seperated list of exposed functions.

The proposal currently suggests that "exposing:" is to be used to make a function available from a different module without requiring a module or alias prefix. This use will now be enabled using the "import:" keyword.

If "exposing:" is not present in a module definition, all functions are exposed.

Unresolvable modules will still be possible, but their use is now limited to prototyping a new module layout, instead of playing any role in exposing functions. A top-level module definition is required to specify unresolvable modules.

Different resolutions for first- and third-party modules

The developer will now be required to specify whether a local or a third-party module is being referenced. A module from a third-party package is now referenced with a leading slash, while local modules are referenced without a leading slash.

This means that

  • /some/module references a module in a third party package
  • some/module references a module in the current project

This has two benefits:

  • It is now more clear to the reader of the code where to find certain modules
  • It is less likely with module name collisions when importing many packages

Function-local imports and aliases

Each function definition is now allowed to specify its own imports: and alias: keywords. The motivation for this stems from my own experience writing Elm code.

When writing a function/constant which essentially is a Html or Css definition, it makes sense to expose all functions of the relevant packages as the context of those functions makes it pretty clear what "width" or "class" mean. However, because both those packages expose functions with common names (like "node", "height", "size" etc.) it's rarely a good idea to actually expose all functions within those modules.

With support for local imports and aliases, this would be simpler:

# Functions of /html and /html/attributes now only imported within this function definition

def: app-view imports: /html imports: /html/attributes : button on-click [ do-something ] >attribute style "background: red" >attribute "Click me" >text

In comparison to using module-wide aliases:

defmodule:
alias: attributes /html/attributes
:

def: app-view : /html/button attributes/on-click [ do-something ] attributes/>attribute attributes/style "background: red" attributes/>attribute "Click me" /html/>text

Entry point override

As mentioned in the proposal, an entry point can be provided to the compiler. It should also be possible to define an entry point in the play.json file for the project. In the case where one entry point is defined both in play.json and as an argument to the compiler, what is passed to the compiler takes precedence.


robin.hansen added on 2021-06-02 10:03:38:
Modules within modules has been postponed for now.