Closed spy16 closed 4 years ago
I've been thinking of this on my end as well, and agree that such functionality should be exposed.
I saw it as a (.method-or-field target)
form, however, implemented by a ReaderMacro
. I'm not opposed to the .
function approach, but I'm curious why you prefer it?
In my mind, a Go-flavored lisp should aspire to the same minimalism that one finds in Go. One minor annoyance I have with various lisps (including Clojure) is the sheer number of exported symbols in the default namespace. Clojure's core
namespace has over 200 exported functions, for instance! π€’
One of my aspirations for my own project is to keep the language spec very small. In so doing, I hope to attain the same kind of simplicity as one finds in Go, and in order to achieve this, I would argue that we should build a lisp engine that reduces the number of functions that need to be implemented, rather than pushing functionality out to functions. A powerful introspection system for native Go structures seems like an opportunity to do just that.
I'm not sure what else we could be doing to move in this direction, but I'm interested in any thoughts you might have.
Returning to the point at hand, my gut tells me this should not be a function, but I'm open to being wrong! π
Addendum: regarding the point about Go-style simplicity above, I think this also touches upon an important marketing consideration for Slang. If we want this library to take off, it's important that we differentiate it from all the other "make a lisp" libraries on GitHub. A heavy emphasis on making simple (i.e.: small, feature-conservative) lisps is a very interesting niche in this respect. We would benefit from being unique as well as being aligned with the philosophy of the host language (Go).
I have with various lisps (including Clojure) is the sheer number of exported symbols in the default namespace
Same here. That's why i took this decision in fact. My idea was that a minimal scope would export only few selected functions like .
, def
, fn*
etc. and things like cons
, conj
, seq?
etc. would be built using pure lisp and are not included by default.
I'm not opposed to the . function approach, but I'm curious why you prefer it?
To be honest, I am not totally happy with it either, i am definitely open for suggestions. Only reason I picked this was to ensure there is no special handling of things. For example if I picked (.member target)
approach, everytime a list is evaluated, the evaluation logic would have to check if the first item in the list starts with a .
..
It's not possible to do this with a ReaderMacro
in a generic way as it stands, and here's why: ReaderMacro
is completely decoupled from a Scope
, so it has no knowledge of what target values are available. Even if we introduce a new member access
expression/form that reader emits, we will also have to provide a way to construct this expr without reader (lisp principle: code and data are not different)..
I can think of following alternatives here:
Reader
depend on a Scope
and make it emit the member being accessed as the value from the reader. (target.member args...)
kind of access which would make it simpler (but need to handle ambiguity when a symbol itself has a .
in it. ((.member target) args...)
approach I think approach 2 would be the simplest to use in most cases.. But the problem is, we will not be able to use it against inlined values.. i.e., ((.Cons []) 1)
can be done with approach 3 for example, but in approach 1 & 2 it would be ([].Cons 1)
which is ambiguous for parsing..
A powerful introspection system for native Go structures seems like an opportunity to do just that.
This is what i was going for too.
I was thinking of simplicity in terms of design of Sabre as library itself: Provide a bare-bones environment which can be built upon.
Enable target.member kind of access which would make it simpler, but we will lose capability to have nested namespaces like sabre.core.math (which i think is not necessary, let me know what you think)...
An alternative would be to take Python's approach and model namespace lookups as regular object lookups. Imagine you have a namespace with a foo.bar
symbol that maps to a type t struct { Baz int }
. The expression foo.bar.Baz
could be supported by the following algorithm:
foo.bar
o
. foo.bar
off of the fully-qualified lookup symbol, foo.bar.Baz
. The result is Baz
.o
instead of the namespace, until the the algorithm terminates.Honestly, I really like this approach:
/
and .
) in order to resolve symbols.This is one of the things I think Python does extremely well. This kind of encapsulation is (IMHO) the only real upside to modeling data as objects. Provided we don't encourage stateful objects, I see no downsides.
This is what i am going for too. In fact now that member access is allowed, i was considering to implement things like cons, conj etc. through this.
+1 for this.
I was thinking of simplicity in terms of design of Sabre as library itself: Provide a bare-bones environment which can be built upon.
Yes, I think this is also very important. My suggestion was more of a supplement than an alternative. Point is: I'm favor of strategically adding a little bit of complexity in a select few places in order to encourage the design of simple languages.
Enable (target.member args...) kind of access which would make it simpler (but need to handle ambiguity when a symbol itself has a . in it.
Actually, forgot to correct this part. This can be handled like you said and i really like this too.. It's how Go does it pkg.Symbol.Member.SubMember
etc. it's how Python does it.. But one problem i see is w.r.t., member access on literal values.. for example, to access Cons
method of a vector/list literals.. Like ([].Cons 1)
which would be hard to parse.. This is not a major problem though if we think it's okay to add some additional boilerplate in case of literals.. i.e., (let [empty-vec []] (empty-vec.Cons 1))
..
What do you think ?
This can be handled like you said and i really like this too..
Awesome - then consider me onboard π
But one problem i see is w.r.t., member access on literal values..
Just to make sure I understand, the difficulty is exclusively at the parsing level because it involves some form of lookback / context? For example, foo.bar
is easy to parse because foo
can be located in the namespace. However [].bar
is hard because there is no []
symbol anywhere in the namespace.
Assuming I've understood the issue, I'm 100% onboard with your proposed mitigation strategy (which is basically to not worry it for now). It would nevertheless be good to handle this more elegantly in the mid-to-long term. Perhaps we can open an issue and get back to it later?
However [].bar is hard because there is no [] symbol anywhere in the namespace.
Not quite. Issue is that the moment reader sees [
it drops into the logic that parses a vector and stays there until it reaches a matching ]
and when it does, it's considered end of one form. Following .
will not be considered as part of this. Instead another symbol .Cons
will be emitted . Instead of having a form that expresses [].Cons
you'll end up with 2 forms a list []
and a symbol .Cons
..
But I think it's best not to worry about this now at all. Gain in simplicity is bigger compared the loss in this minor detail.
Ah yes, of course! Ok, makes sense.
But I think it's best not to worry about this now at all. Gain in simplicity is bigger compared the loss in this minor detail.
Totally agree.
Cool. I'll try to get this done when I get sometime. Thanks for bringing this up.. I got caught up in doing things similar to Clojure I guess.. π
Thanks for bringing this up.. I got caught up in doing things similar to Clojure I guess.. π
No sweat - I'm happy to be part of the design process :)
Just to put the above discussion into final points:
.
as the delimiter and do recursive lookups until termination. And i think it would be nicer to allow kebab case naming for member access. (i.e., if the method of a type Foo
in Go is named GetTheName
, from Sabre it would be better to have foo.get-the-name
kind of access)... I am considering may be both foo.GetTheName
andd foo.get-the-name
should work. Let me know what you think..
Also, I am thinking no dots allowed in symbols similar to all other languages. Clojure allows this but in a weird & useless way: (def hello.world 10)
works and creates a binding, but when you try to use the symbol hello.world
somewhere it just throws ClassNotFoundException
.. I can't really think of a use-case where using .
in names can make it better/readable. But allowing it can be confusing (For example when i type foo.bar.baz
, it's not really clear if it is resolving baz
of foo.bar
or is foo.bar.baz
itself is a value?)..
πFor points 1 & 2.
And i think it would be nicer to allow kebab case naming for member access.
π
I am considering may be both foo.GetTheName andd foo.get-the-name should work. Let me know what you think..
I'm very much in favor of this. Kebab-case is certainly convenient, but forcing case-conversion adds cognitive overhead for users, and may at times be more confusing than the native names.
Also, I am thinking no dots allowed in symbols similar to all other languages.
Strongly agree. Dots have special meaning. Disallowing their use in symbols helps signal intent. Furthermore, we should avoid letting people abuse dots in weird ways in case we need to make changes to field lookup semantics in the future.
Overall: π
This has been implemented. I have not done support for kebab case yet. For now going to keep only native names.
Sabre should be able to access member fields/methods of values bound in the scope.
Clojure uses
(.MethodName target)
or(.-FieldName target)
for method and field accesses repsectively.I am thinking this differentiation is not required in Go since Go types cannot have method+field of same name. ~But
(.MemberName target)
seems like a reasonable choice.~ In favor of keeping special handling to minimum, i am leaning towards exposing a special.
function(. member-name target)
which finds the member of the target with the given name and returns it,Any thoughts on this ? @lthibault