Improving the external function feature #2133
Replies: 3 comments 6 replies
-
Perhaps If conditional compilation is to remain, it could then use the Then the const target =
@erlang "Erlang"
@javascript "Javascript"
@_ "Unknown"
pub type NoReturn
/// Halts the runtime.
///
@erlang external "erlang" "halt"
@javascript external "" "process.exit"
pub fn exit() -> NoReturn {
NoReturn
} or @target erlang import gleam/erlang/os
/// Returns a list of argument strings passed when invoking the program.
///
@target javascript external "./mod.mjs" "arguments"
pub fn arguments() -> List(String) {
@target erlang { os.start_arguments() }
@target _ { [] }
} I also wonder about the distinction between function attributes and current language keywords. Perhaps all functions should use only the |
Beta Was this translation helpful? Give feedback.
-
Hi @lpil, felt like adding my 5 cents here. Why is it a problem to call "external" functions? Is it because of 2 different targets supported - erlang and javascript? Because if we're speaking about the erlang target only, as far as I know, both erlang and gleam are compatible without any intermediate layer required. I think it would be best if one could just import whatever function from an erlang module and call it. Without any "external" declarations needed. Are there any troubles with it? |
Beta Was this translation helpful? Give feedback.
-
I really like the way this unifies external and regular functions. I guess we could do something similar with types. @external(javascript)
pub type Date {
Date(milliseconds: Int)
}
@external(javascript, "./date_ffi.mjs", "parse")
pub fn parse(str: String) -> Date {
// implement 8601 date parsing in gleam
} This is safe if you treat Allowing different imports on different platforms seems like a headache. I would prefer platform-specific blocks, maybe something like the following: import gleam/erlang/os.{start_arguments as erlang_argv}
import gleam/nodejs.{argv as node_argv}
pub fn main() {
let arguments = @target {
javascript -> node_argv()
erlang -> erlang_argv()
_ -> []
}
} A |
Beta Was this translation helpful? Give feedback.
-
Gleam features FFI in the form of "external functions". It also has conditional compilation (
if erlang {
) so that Gleam projects that support multiple targets can use FFI with one external function definition per target.Problems with the current design
There are several small annoyances with the current design.
No way to infer and communicate the supported targets
If writing a library that only supports one target there are two options, either wrap all the code in each module in conditional compilation, or to not use conditional compilation at all. Both approaches have downsides.
Adding conditional compilation to each module is tedious and error prone, and if the package is added as a dependency to a project using an unsupported target then when it is compiled a cryptic error will be produced about a random value or type not existing for modules imported from that package.
Not using conditional compilation is less irritating to the package author, but using the package with an unsupported target will result in either a runtime error for JavaScript, or a compile time error for Erlang from the Erlang compiler.
In both instances the user gets a very confusing error, and we don't have a good way to identify when a package is not compatible with a target.
Diverging module interfaces
With the current system is it possible and easy for modules to expose different functions and types depending on the target, either intentionally or by accident.
In this example one implementation returns a
BitString
and the other returns aString
. In a real module with much more code it would be harder to spot this, and there is no aid from the compiler here.We want to discourage modules having different interfaces for different targets as it makes them harder and more confusing to use, and we want it to be easy for the author to keep the interfaces consistent.
Verbosity
Ignoring comments, in the first example there 13 lines, and only 3 of them convey any information about the code (the function signature, and the source of the external implementations).
The function signature is written 3 times, and the function name is written 4 times (albeit with the meaningless
do_
prefix for 3 of those times).No good home for documentation
Documentation is written in
///
comments in Gleam on the line above the item they document.Due to there being multiple implementations of the same function there is no one place for the documentation to be placed, so it has to be duplicated, or a wrapper function has to be written for the documentation to be attached to, as in the example above.
Duplicating documentation is verbose and tedious, and is it easy for each comment to get out-of-sync. Wrapping functions is easier to manage but also verbose compared to documenting one function, and it has a small runtime performance cost.
Labelled argument syntax confusion
This one is not specific to multi-target projects, but a common mistake with external functions is to give the arguments labels unintentionally.
In this code the argument has been given the label
s
, but often the programmer intended to give the argument the names
and leave it unlabelled. This confusion likely stems from the syntax for an external function's argument labels is the same as a regular function's argument name.The proposal
The proposal is to remove the dedicated "external" syntax, and instead have a single function declaration syntax that can be used for both regular functions and external functions, with explicitly listed targets. This syntax would be a replacement for both the external function syntax and conditional compilation blocks.
Note: The syntax here is a placeholder. We can determine the exact syntax later if we decide this new approach is a good idea.
Here is the first example in the current system and then the new proposed system.
A function header is used to declare the function's name and its type signature, but it is absent a body. Attributes are used to declare the external implementations for the function, each explicitly listing the target.
The syntax is much more concise, and it is clear where the documentation should be placed. There is no longer a difference in syntax between regular functions and external functions, so the confusion around labelled arguments is removed.
When compiling this code we can check the given implementations and return a helpful error if the current target is not supported.
We can determine the supported targets for a package by scanning the source code and keeping track of the targets that have been declared for all FFI. This information could be presented on https://packages.gleam.run/ and permit uses to search for packages that support their desired targets.
Mixing regular and external function definitions
In some cases we wish to use an external definition only for some targets, using a pure Gleam implementation for others. One such example is the
list.length
function, which is implemented in Gleam for JavaScript, but uses runtime'slength
function for Erlang as it is implemented in C and is faster.Here the function has a body as well as an external implementation for Erlang. For the Erlang target the external implementation will be used, and for other targets the pure Gleam implementation will be used.
External types
Given
external
would no longer a keyword for function I think we would also want to remove it as a keyword for types, instead specifying the type with no constructors.Future uses for the attribute syntax
An attribute syntax as proposed here can also be used for other features such as marking functions as deprecated, and supplying aliases for use in generated HTML documentation's search.
Problems with the proposal
As raised by @tynanbe sometimes platform specific code isn't directly using native target code via external functions, instead it is using different target-specific Gleam modules.
This proposal is currently insufficient to support this. What could we do here?
Beta Was this translation helpful? Give feedback.
All reactions