Extending behaviour of Janet objects with respect to standard library functions #581
Replies: 3 comments 8 replies
-
Actually, one thing I feel we should establish is: @bakpakin , I've been assuming the answer to this, but do you currently feel that this is an area of the language worth extending? Currently, Janet stands at around the Closed functions, open methods stage: OO via tables with keyword methods is available and supported, but abstract types/non-table types are not extensible, and certain potentially generic behaviours like I personally feel like there are some changes that could be made which, even if they don't add a lot of new functionality, could consolidate some of the current differences in behaviour between tables/non-tables on one hand, and global functions/local methods on the other. But of course, I'd want to validate that feeling with you before we all spent a lot of time arguing about what those changes should be. |
Beta Was this translation helpful? Give feedback.
-
I think
For set-method, on the other hand, you need somewhere to store function pointers. For tables, I suppose you could store it in the prototype table if it existed, and for abstract types that could be yet another function pointer. However, I'm not sure that this would really even be needed. |
Beta Was this translation helpful? Give feedback.
-
Closely related to this topic -- I have just implemented a Proof of Concept module called janet-abstract (https://github.com/MikeBeller/janet-abstract). It provides a new abstract type that lets you create tables in Janet which can hook the 'next', 'get', 'put', 'tostring', 'compare' functions in the JanetAbstract struct, and write the functions in Janet in the table's prototype. I think it's something @bakpakin referred to in the past as a Proxy. I then used janet-abstract to upgrade my janet-set module (https://github.com/MikeBeller/janet-set) to have much better semantics for iteration, printing, and things like 'length'. Check it out if you have a chance. I had one major learning -- because the "get" function in abstract types is shared between getting methods from the method table, and other uses, you can not easily create an abstract container type (whether in C or Janet) which supports keywords as a key type. Fuller discussion in the readme to janet-abstract. |
Beta Was this translation helpful? Give feedback.
-
There has been lots of interesting discussion in the chat and elsewhere about different approaches to extending Janet - protocols, multimethods, user-defined types, and other. It's useful to disambiguate between related problems. I'll try to summarize the discussion around one of them—one that is maybe the most-discussed—here.
Problem: extending the behaviour of primitive Janet functions over new terms
Many of the core behaviours in Janet are accomplished by providing a sensible, composable implementation of a standard library function for some data type. To give an example: to perform iteration, call
next
repeatedly on the term you want to iterate over. Thus, the many flavors of iteration are decomposed into the more tractable problem of defining a semantics ofnext
for some type.In general, we are talking about the expression problem. We would like the ability as Janet developers to introduce both new behaviours, such as iteration, and new data types. We would like the ability to implement new behaviours for existing data types as well as include new data types for existing behaviours.
Methods vs. Functions
There is a little bit of inconsistency in the overall standard lib in term of implementation details. Sometimes, the function needing to be implemented is "open", in the sense that it's a method that should be inserted into a table (eg.,
:close
), and sometimes it's "closed" in the sense that the function needing to be implemented is a function, likenext
, which needs to have sensible semantics implemented at the language level.Solutions
The above two existing approaches are individual examples of a spectrum of possible solutions to the expression problem. Here is a rough overview:
next
depends only on its definition in the Janet source.next
function, but instead always by calling:next
.next
). However, any behaviour in either category depends on keyword methods.next
, for instance, should call:next
on its object if that method is available.next
). However, any behaviour in either category depends on an independent methods namespace. Methods are added to any term, table or not, by special functionsget-method
andset-method
.(:next my-object)
would call the:next
method onmy-object
, but not by getting the value at:next
in mymy-object
. It would look for a value bound to:next
in the special methods namespace and if that value is present, call it withmy-object
as the argument. The prototypes system would be extended to dispatch to a table's prototype in bothget
andget-method
calls. The methods namespace covers at least abstract types, in addition to tables, so that methods could be set on non-tables.Some Tradeoffs
Some discussion has taken place about the "magic methods" approach.
Single-namespace methods
Janet's current behaviour defines a method as a function that's accessible on a table by
get
. This has the advantages of keeping the semantics extremely simple and predictable;(:foo bar)
is identical to((bar :foo) bar)
. Similarly, to define a method for a table, simply put a function in it.This has the drawbacks of a relatively high likelihood of collision. Because methods are values with normal names in tables, it is very straightforward to accidentally overwrite a table's method by binding a new value to that name in the table. There are methods to reduce, if not eliminate, this likelihood: Python uses the
__foo__
convention for its magic methods, making it unlikely that a developer will unknowingly clobber one of those values. Using globally-unique values that are bound to global symbols (for instance, binding some unique value like(def methods/next @"")
in the root env and then calling(get methods/next my-object)
is another approach.This has the more serious drawback of restricting extensibility to objects with table-like semantics; for instance,
(put methods/to-msgpack @[] (fn ...))
doesn't make very much sense, since arrays can't associate names to values (only indices), so it they would be too semantically poor to support behavioural extension.Separate-namespace methods
The two drawbacks of single-namespace methods, as described above, could be avoided by implementing an entirely new method semantics. Adding
set-method
andget-method
to the language would ensure that no matter what values were associated in a table, they wouldn't overwrite methods. And since they didn't rely on the existing semantics of associative data structures, they could be meaningfully implemented for any data type, not just tables.The main drawback here would be that same point: the introduction of an entirely new semantics/namespace into the language. Existing intuitions would not apply to the methods associated with objects and existing functions, for instance, pretty-printing in a REPL, would not "see" the methods in this namespace. This has the potential of adding a sizeable dimension to the language, or the risk of adding a difficult-to-see layer of behavior.
Protocol Targets
For a productive protocols system, we would need the ability to define protocol implementations for any term, not just tables. For instance, given an
Iterable
protocol, I would require the ability to implementIterable/next
for arrays and tuples, just as I'd require the ability to implementIterable/next
for my newDog
prototype table. However, given two invocations of the form(Iterable/next @[:a] nil)
and
(Iterable/next (table/setproto Dog @{:name "Fido"}) nil)
The definition of
next
can be sufficiently sophisticated as to dispatch conditionally based on the type of the argument rather than always reduce to a method call. Therefore, an implementation definition like(defimpl Iterable/next Array [self] <function body>)
can reduce to a function body to be evaluated if the object is an array, without having to define an Array prototype or involve tables in any way.Beta Was this translation helpful? Give feedback.
All reactions