Jump to content

Wikifunctions:Execution targets

From Wikifunctions
Technical documentation about Wikifunctions
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/Implementations. 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 strs 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.

Type conversion in function executors
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]
])
  1. 1 2 ZReference is a custom-defined class in Python and JavaScript.
  2. 1 2 ZPair is a custom-defined class in Python and JavaScript.

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.