Arc Forumnew | comments | leaders | submitlogin
2 points by Pauan 4286 days ago | link | parent

"I'm reminded of our argument many months ago about a type system where you could build table-like entities using duck typing[1]."

Yeah, but in retrospect it was an awful idea. My latest ideas for Nulan are soooo much better than my old ideas. I still think my old idea would have been better than Arc's annotate/type/rep, but...

1) It was too complicated: something that should be in a library, not in the core

2) It used hardcoded strings/symbols for the keys... something I've really tried to avoid lately. My new idea for Nulan has zero hardcoding: it's all done based on values and positions

3) It was a bit too OOP for my tastes... even back then. But at the time it was the best idea I had for extensible data types

---

"rocketnia hates isa too, iirc :)"

Well, I'm not rocketnia, but I think that's because:

1) It uses symbols for types, at least in Arc 3.1

2) It plays poorly with ssyntax, since you can say foo?.x but you have to say (isa x foo)

3) If you want to add new types that behave like existing types, you need to always extend isa (or type), but with proper foo? functions you only need to extend those individual functions

This suggests that having string?, cons?, etc. rather than (isa ... 'string) and (isa ... 'cons) is a good idea.

But I take it a step further in Nulan and also say that annotate/rep are poor because if you want to tag something with multiple types, you need to wrap it repeatedly: (annotate 'foo (annotate 'bar ...))[1] and then you need to unwrap it repeatedly: (rep (rep ...))

This also means that annotate is not transparent: an annotated thing doesn't behave the same as a non-annotated thing... quite a while back waterhouse talked about having the base primitives like car/cdr/etc. automatically call rep: that way you could annotate things and they'd transparently work with existing stuff.

But as rocketnia pointed out, sometimes you don't want that. So there are times where you want something to behave transparently and other times where you don't want that. I think my type system can accomodate that easily.

---

In any case, I've become pretty convinced that Paul Graham was right at least in theory: you only need the theoretical equivalents of annotate/type/rep to implement any type system you want. The problem is that they were implemented poorly in Arc 3.1, not that the idea itself is bad. Nulan's (current) type system is theoretically equivalent to Arc's annotate/type/rep but I believe it's far superior in implementation.

---

* [1]: Alternatively, you could use a list of types: (annotate (list 'foo 'bar) ...) but that doesn't work with isa, at least not without retrofitting it, and it still doesn't solve the transparency problem.



2 points by rocketnia 4284 days ago | link

"Well, I'm not rocketnia, but I think that's because"

You're a pretty good representative of... my past self. ^_^

---

"1) It uses symbols for types, at least in Arc 3.1"

Yep, there's no need to have first-class type tags of any sort when first-class wrapper and unwrapper functions could do that job.

---

"2) It plays poorly with ssyntax, since you can say foo?.x but you have to say (isa x foo)"

Nowadays I'd rather design the technical parts of the system first and then design the syntax to support it the best it can.

---

"3) If you want to add new types that behave like existing types, you need to always extend isa (or type), but with proper foo? functions you only need to extend those individual functions"

These days I would rather give library writers the ability to say what's extensible and what isn't. If it isn't extensible enough to support a new type, too bad. Fork the library.

This applies to languages too. If the language isn't extensible enough, fork it. I once wanted to design languages such that nobody would need to fork them, but I don't put much faith in that anymore, so I see it as a lower priority.

Anyway, I don't like Arc's 'type and 'rep because even though they can be used on any value, that misses the point. If I want to make a value that anyone can take apart, I'll use a table or a cons cell. Type wrappers are good for setting aside new areas of value space that _no_ existing utility knows how to handle yet.

--

"But I take it a step further in Nulan and also say that annotate/rep are poor because if you want to tag something with multiple types, you need to wrap it repeatedly: (annotate 'foo (annotate 'bar ...)) and then you need to unwrap it repeatedly: (rep (rep ...))"

In that particular case, I would consider making an 'unwrap-bar function and extending it to handle both 'foo and 'bar.

I would probably name 'unwrap-bar more descriptively, reducing its association with the concrete implementation details of the 'bar type, if only these weren't metasyntactic variables. :-p

Anyway, what I'm doing isn't necessarily strictly better than what you're doing. We're thinking of different overall systems.

-----

1 point by akkartik 4284 days ago | link

" I once wanted to design languages such that nobody would need to fork them, but I don't put much faith in that anymore, so I see it as a lower priority."

Welcome! :)

-----

1 point by rocketnia 4284 days ago | link

Er, welcome what? XD

-----

3 points by akkartik 4284 days ago | link

To the club :) It's a pretty select club, I think. Too much effort is expended to avoid forking in the name of 'reuse', IMO.

But reuse in what context? Reuse all across the universe? I think the notion of reuse is often ill-posed. The only reuse that matters is in the context of a codebase. And as long as a codebase only has one fork of any software you can have unlimited forking in the world without reducing reuse.

(This also kinda feeds into our conversations about namespaces and libraries and backwards compatibility.)

-----

2 points by Pauan 4283 days ago | link

"(This also kinda feeds into our conversations about namespaces and libraries and backwards compatibility.)"

Well, I only care about libraries because it means that (theoretically) one guy can write the code and a bunch of people can use it. The same is true of languages.

This is nice for things that are pretty stable, like regexps. Everybody knows regexps. The syntax is reasonably standardized across implementations. It doesn't make sense to have everybody write their own custom regexp implementation: just write one solid one and use it as a library.

But that only works when there really is a Single Right Way to do it. As soon as there's different goals and priorities, you just end up with a lot of little custom libraries (or lots of forks of libraries), in which case they might as well not be libraries to begin with, since they're only really useful to their original author.

This means you should only write libraries when there's some sort of standard or consensus. If there isn't, just write your code in a very flat simple style, no libraries needed.

---

As for namespaces... I don't actually care about those for code reuse. Libraries handle code reuse just fine with or without namespaces: look at Emacs Lisp as an example of a language with no namespaces and dynamic scope yet they seem to manage okay. C also has lots of libraries and code reuse yet doesn't have namespaces.

The reason I care about namespaces is that it makes certain things easier to reason about, that is, it reduces the cognitive load needed to design and understand a program. It also makes your programs shorter because when conflicts do arise, you don't need to do things like prefixing all your global variables, like they do in C/Emacs/Python/JavaScript/etc.

Well, that's the theory, anyways... in practice, I agree with you that traditional namespaces are overrated and only mildly helpful while requiring a lot of infrastructure to support them. An overall net loss that can only be recouped by writing many libraries over a long period of time that make use of the namespace system.

But I think Nulan is a bit unique in that it doesn't have traditional namespaces, but its immutable environments give you simple partitioning that I believe reduces cognitive load while not restricting you too much.

-----

1 point by rocketnia 4283 days ago | link

I've responded in a new thread, "Some plans for code reuse": http://arclanguage.org/item?id=16668

-----

2 points by Pauan 4286 days ago | link

By the way... because they're (mostly) transparent and you can easily have multiple types attached to a single blob of data, you can use the type system to attach any arbitrary metadata you want. I can't help but feel that there's some connection there to monads...

I also just realized it might have some interesting uses as an alternative to exceptions. Rather than throwing an exception, you would instead return an object that has been tagged with an "exception" type.

By default this exception type would be propagated upwards until it reaches the top of the scope, but you can catch it by using pattern matching:

  $let (exception? X) ...
    ...
I still have no clue how to handle errors in Nulan, though... error values, exceptions, Maybe type, something else...

-----

3 points by rocketnia 4284 days ago | link

"By the way... because they're (mostly) transparent and you can easily have multiple types attached to a single blob of data, you can use the type system to attach any arbitrary metadata you want. I can't help but feel that there's some connection there to monads..."

Yep! There's a pretty close connection to comonads, the opposite of monads. Comonads are for structures that can be unwrapped into a value at any time, but which can also be transformed without unwrapping by... confusingly, sending them a function that would unwrap them.

  extract :: MyWrap x -> x
  extend :: (MyWrap x -> y) -> MyWrap x -> MyWrap y
The point is that the wrapper that appears around y is generally related to the wrapper x started with. Note that extend's function argument might actually process multiple (MyWrap x) values, all hidden inside the main (MyWrap x).[1]

To illustrate, a pair of a Kernel expression and a Kernel environment can be used as a comonad, as long as we design Kernel expressions so that they can reify arbitrary values using 'quote. The extract method is eval, and the extend method bundles up the result as a literal expression together with the original environment.

[1] Aside from a few laws this has to follow, this kind of variation of behavior is particular to the implementation of each comonad. There will typically be some special-case utilities outside the comonad interface that take advantage of these specific features.

-----

2 points by rocketnia 4284 days ago | link

"I also just realized it might have some interesting uses as an alternative to exceptions. Rather than throwing an exception, you would instead return an object that has been tagged with an "exception" type."

This is monadic. Generally if you build a program out of a lot of functions of type (x -> MyWrap y) you can probably treat MyWrap as a monad, whereas if you build it out of a lot of functions of type (MyWrap x -> y) you can probably treat it as a comonad.

---

"By default this exception type would be propagated upwards until it reaches the top of the scope, but you can catch it by using pattern matching"

In this case you might be implementing it in a monadic way, but for most purposes the language user might as well treat it as a regular side effect.

I've been pondering the question of how to make the syntax of a language generic enough to integrate user-defined side effect models as though they're built in, just like this. However, this is a rabbit hole which has prevented me from getting very much done. :-p It's also straying pretty far from this thread's topic.

-----