Wikifunctions:Execution targets
General overviews |
---|
User-facing features |
Back-end specifics |
Definition
Execution targets are well-formed code snippets written in "conventional" programming languages (e.g. Python), as opposed to the bespoke composition language implemented in Wikifunctions. Execution targets are useful for writing complex, self-contained algorithms, especially where performance is important.
Current status
How function execution works
In Wikifunctions, the Z8/Function
is the primary object of concern. A Z8/Function
can have multiple Z14/Implementation
s. Every Z14/Implementation
attached to a given function should behave identically with respect to inputs and outputs, though they may differ vastly in terms of their performance characteristics. An implementation can be built-in (defined as part of the core Wikifunctions runtime), evaluated (run as an execution target), or a composition (implemented by arbitrarily nested calls to other functions). Evaluated implementations are of interest here.
An evaluated implementation packages all required information about a function call and sends it to the function evaluator. The function evaluator has executors that run the included code snippet as appropriate for the requested programming language. For example, if an implementation declares that it is in Python, the orchestrator will send to the Python evaluator the following information:
- the Python source code string;
- the arguments to the function;
- for each argument, the Python source code string for the relevant type converters to code (if applicable);
- for the return value, the Python source code string for the relevant type converters from code (if applicable).
Following are some definitions to supplement the above example.
Supported programming languages
At present, Wikifunctions supports function execution in Python and JavaScript. Wikifunctions uses, respectively, the RustPython implementation of Python and the QuickJS engine for JS. These implementations and engines have been chosen due to better support for WebAssembly. In future, the list of supported languages (and language versions!) will grow.
Type conversion
Overview
Type converters transform data between the ZObject representation and the natural idioms of a given programming language. Type converters allow for source code in a programming language to operate over data in a convenient way, avoiding a great deal of boilerplate code.
For example, in ZObject normal form, a list containing the single string "😤Wikifunctions😤"
looks like
{
"Z1K1": {
"Z1K1": {
"Z1K1": "Z9",
"Z9K1": "Z7"
},
"Z7K1": {
"Z1K1": "Z9",
"Z9K1": "Z881"
},
"Z881K1": {
"Z1K1": "Z9",
"Z9K1": "Z6"
}
},
"K1": {
"Z1K1": "Z6",
"Z6K1": "😤Wikifunctions😤"
},
"K2": {
"Z1K1": {
"Z1K1": {
"Z1K1": "Z9",
"Z9K1": "Z7"
},
"Z7K1": {
"Z1K1": "Z9",
"Z9K1": "Z881"
},
"Z881K1": {
"Z1K1": "Z9",
"Z9K1": "Z6"
}
}
}
}
Type converters allow this rather cumbersome representation to be realized, simply, as
["😤Wikifunctions😤"] # list with a single string
in Python, or as
["😤Wikifunctions😤"] // list with a single string
in JavaScript.
Default type conversion
For a small set of built-in types, the executions provide default type converters. These type converters automatically infer how to convert from ZObject to code (based on the object's Z1K1/Type
) and from code to ZObject (based on object introspection). For example, in Python, if a function receives an object like
{
"Z1K1": "Z6",
"Z6K1": "STRING"
}
as input, then the default type conversion will infer from Z1K1 => Z6
that this object is a Z6/String
, and convert the input to a Python str
:
"STRING"
Note that these determinations are made recursively where necessary, as in the list example above. In a more extreme case: an object with type Z883(Z6, Z881(Z6))
will become, in Python, a dict
whose keys are str
s and whose values are list[str]
s.
Working in reverse, default type conversion from code to the ZObject representation will check the types of objects (with a given programming language's introspection capabilities). str
in Python or String
in JavaScript will become a Z6/string
. In the nontrivial case, a Python dict{str:list[str]}
or a JavaScript Map<String, Array[String]>
will become a ZObject whose type is given by Z883(Z6, Z881(Z6))
.
Following is a table of ZObject types and the corresponding types in Python and JavaScript.
ZObject type | Corresponding Python type | Corresponding JS type |
---|---|---|
Example ZObject instance | Example Python instance | Example JS instance |
Z6 |
str
|
String
|
{"Z1K1":"Z6","Z6K1":"strang"}
|
"strang"
|
"strang"
|
~ | ~ | ~ |
Z9 |
Z9Reference [Note 1] |
Z9Reference [Note 1] |
{"Z1K1":"Z9","Z9K1":"Z10000"}
|
Z9Reference("Z10000")
|
new Z9Reference("Z10000")
|
~ | ~ | ~ |
Z21 |
NoneType
|
- |
{"Z1K1":{"Z1K1":"Z9","Z9K1":"Z21"}}
|
None
|
null
|
~ | ~ | ~ |
Z40 |
bool
|
Boolean
|
{
"Z1K1": {
"Z1K1": "Z9",
"Z9K1":"Z40"
},
"Z40K1": {
"Z1K1":"Z9",
"Z9K1":"Z41"
}
}
|
True
|
true
|
~ | ~ | ~ |
Z881(Z?) |
list[?] |
Array[?] |
{
"Z1K1": {
"Z1K1": {
"Z1K1": "Z9",
"Z9K1": "Z7"
},
"Z7K1": {
"Z1K1": "Z9",
"Z9K1": "Z881"
},
"Z881K1": {
"Z1K1": "Z9",
"Z9K1": "Z6"
}
},
"K1": {
"Z1K1": "Z6",
"Z6K1": "stronk"
},
"K2": {
"Z1K1": {
"Z1K1": {
"Z1K1": "Z9",
"Z9K1": "Z7"
},
"Z7K1": {
"Z1K1": "Z9",
"Z9K1": "Z881"
},
"Z881K1": {
"Z1K1": "Z9",
"Z9K1": "Z6"
}
}
}
}
|
["stronk"]
|
["stronk"]
|
~ | ~ | ~ |
Z882(Z?,Z?) |
ZPair [Note 2] |
ZPair[Note 2] |
{
"Z1K1": {
"Z1K1": {
"Z1K1": "Z9",
"Z9K1": "Z7"
},
"Z7K1": {
"Z1K1": "Z9",
"Z9K1": "Z882"
},
"Z882K1": {
"Z1K1": "Z9",
"Z9K1": "Z40"
}
},
"K1": {
"Z1K1": "Z6",
"Z6K1": "stronk"
},
"K2": {
"Z1K1": "Z9",
"Z9K1": "Z41"
}
}
|
ZPair("stronk", True) |
new ZPair("stronk", true) |
~ | ~ | ~ |
Z883(Z?,Z?) |
dict |
Map |
{
"Z1K1": {
"Z1K1": {
"Z1K1": "Z9",
"Z9K1": "Z7"
},
"Z7K1": {
"Z1K1": "Z9",
"Z9K1": "Z883"
},
"Z883K1": {
"Z1K1": "Z9",
"Z9K1": "Z6"
},
"Z883K2": {
"Z1K1": "Z9",
"Z9K1": "Z40"
}
},
"K1": {
"Z1K1": {
"Z1K1": {
"Z1K1": "Z9",
"Z9K1": "Z7"
},
"Z7K1": {
"Z1K1": "Z9",
"Z9K1": "Z881"
},
"Z881K1": {
"Z1K1": {
"Z1K1": "Z9",
"Z9K1": "Z7"
},
"Z7K1": {
"Z1K1": "Z9",
"Z9K1": "Z882"
},
"Z882K1": {
"Z1K1": "Z9",
"Z9K1": "Z6"
},
"Z882K2": {
"Z1K1": "Z9",
"Z9K1": "Z40"
}
}
},
"K1": {
"Z1K1": {
"Z1K1": {
"Z1K1": "Z9",
"Z9K1": "Z7"
},
"Z7K1": {
"Z1K1": "Z9",
"Z9K1": "Z882"
},
"Z882K1": {
"Z1K1": "Z9",
"Z9K1": "Z6"
},
"Z882K2": {
"Z1K1": "Z9",
"Z9K1": "Z40"
}
},
"K1": {
"Z1K1": "Z6",
"Z6K1": "Hello"
},
"K2": {
"Z1K1": "Z9",
"Z9K1": "Z41"
}
},
"K2": {
"Z1K1": {
"Z1K1": {
"Z1K1": "Z9",
"Z9K1": "Z7"
},
"Z7K1": {
"Z1K1": "Z9",
"Z9K1": "Z881"
},
"Z881K1": {
"Z1K1": {
"Z1K1": "Z9",
"Z9K1": "Z7"
},
"Z7K1": {
"Z1K1": "Z9",
"Z9K1": "Z882"
},
"Z882K1": {
"Z1K1": "Z9",
"Z9K1": "Z6"
},
"Z882K2": {
"Z1K1": "Z9",
"Z9K1": "Z40"
}
}
},
"K1": {
"Z1K1": {
"Z1K1": {
"Z1K1": "Z9",
"Z9K1": "Z7"
},
"Z7K1": {
"Z1K1": "Z9",
"Z9K1": "Z882"
},
"Z882K1": {
"Z1K1": "Z9",
"Z9K1": "Z6"
},
"Z882K2": {
"Z1K1": "Z9",
"Z9K1": "Z40"
}
},
"K1": {
"Z1K1": "Z6",
"Z6K1": "Goodbye"
},
"K2": {
"Z1K1": "Z9",
"Z9K1": "Z42"
}
},
"K2": {
"Z1K1": {
"Z1K1": {
"Z1K1": "Z9",
"Z9K1": "Z7"
},
"Z7K1": {
"Z1K1": "Z9",
"Z9K1": "Z881"
},
"Z881K1": {
"Z1K1": {
"Z1K1": "Z9",
"Z9K1": "Z7"
},
"Z7K1": {
"Z1K1": "Z9",
"Z9K1": "Z882"
},
"Z882K1": {
"Z1K1": "Z9",
"Z9K1": "Z6"
},
"Z882K2": {
"Z1K1": "Z9",
"Z9K1": "Z40"
}
}
}
}
}
}
}
|
{
"Hello": True,
"Goodbye": False
}
|
new Map([
["Hello", true],
["Goodbye", false]
])
|
Custom and non-built-in type conversion
For non-built-in types, both Python and JavaScript additionally support a ZObject
class. For non-built-in objects, ZObject
provides a thin wrapper for arbitrary JSON objects that cleave to the ZObject syntax.
In Python, such an object can be created as follows. Z1K1
should be given as the first argument. All other arguments should be supplied as kwargs
, i.e. by supplying each key as a separate argument to the instantiation of ZObject
. When a ZObject
is type-converted, Z1K1
will be retained as-is, and all other elements will be type-converted according to the above-described logic. For example:
def Z20000():
return ZObject(
{ "Z1K1": "Z9", "Z9K1": "Z10500" },
Z10500K1="a string",
Z10500K2=True )
When type-converted, the result of this function will become
{
"Z1K1": {
"Z1K1": "Z9",
"Z9K1": "Z10500"
},
"Z10500K1": {
"Z1K1": "Z6",
"Z6K1": "a string"
},
"Z10500K2": {
"Z1K1": "Z9",
"Z9K1": "Z41"
}
}
The equivalent incantation in JavaScript is as follows. new ZObject
will construct a ZObject
. The first argument should be a Map
mapping ZObject keys to type-converted values, like the kwargs
above. The second argument should be the Z1K1
. For example:
function Z20000() {
return new ZObject(
new Map( [
[ "Z10500K1", "a string" ],
[ "Z10500K2", true ]
] ),
{ "Z1K1": "Z9", "Z9K1": "Z10500" } );
}
When type-converted, the result of this function will likewise become
{
"Z1K1": {
"Z1K1": "Z9",
"Z9K1": "Z10500"
},
"Z10500K1": {
"Z1K1": "Z6",
"Z6K1": "a string"
},
"Z10500K2": {
"Z1K1": "Z9",
"Z9K1": "Z41"
}
}
Custom type converters allow one to avoid this default behavior. Custom type converters are attached to a Z4/Type
. Z4K7
gives the list of converters from ZObject JSON syntax to code, while Z4K8
gives the list of converters from code to JSON syntax. For the example above, we might define a custom type, e.g., in Python
class MyType:
def __init__(self, thestring, thebool):
self.thestring = thestring
self.thebool = thebool
The type converter to code would then look like
def Z10501(as_json):
return MyType(
as_json["Z10500K1"]["Z6K1"],
as_json["Z10500K2"]["Z9K1"] == "Z41")
And the type converter from code would look like
def Z10502(as_code):
return {
"Z1K1": {
"Z1K1": "Z9",
"Z9K1": "Z10500"
},
"Z10500K1": {
"Z1K1": "Z6",
"Z6K1": as_code.thestring
},
"Z10500K2": {
"Z1K1": "Z9",
"Z9K1": "Z41" if as_code.thebool else "Z42"
}
}
Note that custom type converters are not in any way constrained to be symmetrical: the system does not enforce, for example, that, if MyType
is the target type for conversion to code, it also be the starting point for conversion from code. If custom type conversion is defined in only one direction, the system will attempt to fall back on the default behavior. Currently, any such guarantees have to be managed by the community.
Also note that it's currently necessary to re-implement the default type conversion (e.g., in this case, for strings and Booleans) within the type converter. This is also true for types that contain other types that implement custom type conversion: the entire type conversion function must be implemented for the top-level object.
Desiderata and future work
Composability of executed code
At present, executed code cannot call other Wikifunctions modules. There is provisional support for this in the code, but the feature is far from being production-ready. Eventually, we hope to make it possible for executed code to make arbitrary calls to Wikifunctions.
Composability of type converters
It would be nice if one did not have to rewrite type conversion functions for types that contain other types. At present, it is possible, in some cases, to call the serialize
and deserialize
functions to convert built-in types within custom type converters, but this is unreliable. It also doesn't work for types that implement custom type conversion: the serialize
and deserialize
functions only work with built-in and default type conversion.
At present, there is a single exception to this. In the special case of Lists whose elements define custom type converters, custom type converters defined on the elements are used when type-converting the List. We eventually hope to extend this functionality to the general case. This may involve a loss of type inference functionality, which will solely affect cases where a function defines its inputs/outputs as Z1
.