Ok time for shit loads of waffling about language design because I'm feeling existential about . Let's fucking gooooooo

Ok, so because all my professional and non-newb experience with programming has been with OOP, I naturally want O to be object oriented, and that's fine.

*However*, I've been becoming increasingly interested in functional-style programming (however not pure functional languages). There's this kind of clash between full OOP and functional where in OOP *everything* is a member method, but in functional *nothing* is. (assume 'cont' on all of these)

Show thread

If I compromise then I end up with a language like C++ or D which are definitely not terrible languages, but because they (I'll use D as an example because that's what I know better) mix these 2 approaches it feels clunky in a way, like one approach feels more "correct" and the other feels more "natural", but their combination feels nasty even if it's actually completely fine.

So I'm sort of after this impossible situation where everything's a member *and* nothing's a member.

Show thread

I've found you can divide methods into 2 categories: mandatory members and optional members. A mandatory member comes up when you start using interfaces. If I want some uniform means of working with enumerable things, then I make an interface with 'current' and 'next()' members, so then I can implement whatever kinds of generators and manipulators I want and chain them together however. Those members have to be members though because they're specific implementations of a generic type.

Show thread

I can't inherit a class and write methods that take that subclass, because when my generic method takes input of the superclass/interface, the implementation for *that* is executed, creating a method/attribute that has to be a member as a result of type inheritance and interfaces, both of which are really important things imo for making writing software simpler.

Show thread

On the other side, you have basically *everything else*, where actually there's nothing stopping you from writing a non-member method that just takes a reference to a certain object of the type the method would otherwise be a member of (fuck me that was a sentence).

You then have a subset of these methods where there isn't one single thing of importance, only an action that bases itself on parameters. *These* are the methods that feels off being members.

Show thread

So you have these 2 main categories, the second of which is kind of in 2 rather vague subcategories. Group A *must* be a member, group B *should* be a member, and group C *should not* be a member.

What I want for O is to have a case where there are no "should"s, only "must"s I guess.

The way I've found fully OOP languages like C# get around it is by putting group C in static classes and then putting group B in a mix of both static and normal classes. Messy, but kinda works.

Show thread

*Then* you realise that actually if you can have non-member methods then static classes are just pointless, so now you have *another* thing that's inconsistent and aaaaaaaaaaaa.

I feel like if I could find, figure out, or be taught a way of getting around group A so you can still implement the same stuff in a sane manner without needing member methods, then I could start working well towards a more procedural system that's just structs and functions.

Show thread

Then on the other hand, if I enforce members and static classes I kinda solve the problem as well, and it seems like it would be a simpler method as it's one I understand better. The only things that would ever be external to classes would be enums and extension methods.

But *then* I'm back at the problem where writing functional-style code becomes really iffy.

Show thread

Really though, this kind of two-sides issue is only one of the things I'm really unsure about. I definitely want O to be high-level or at the very least abstracted, but at the same time I want it to be conceptually fairly simple. I feel like I've been putting loads of cool ideas and things into it that are ultimately unnecessary, like value-based declaration conditions when really only type-based conditions are needed.

Show thread

I'm definitely keeping the no-null and default value approach. I want to rethink how I'm doing exceptions and errors though. As meme-y V is at the moment, it's error system is pretty well done from a syntax perspective, so I think I want to create a new system that's still OOP, but works more in that kind of explicit way, where if you call a method that can pass an error, you have to either handle it or explicitly propagate it.

Show thread

So, some things I want to do, mostly based on what I learnt yesterday:

Contract programming: iz gud
Errors: need abandons/panics and checked exceptions and to think about an abort and possibly keeper pattern
Syntax: fuck try/catch blocks, try should only encompass one method call (making it a PrimaryExpression).
Block statements: should be able to just go anywhere (need a good keyword for "returning" a value from these)
Multipart statements: only need if/else now, so I can justify making them special.

Show thread

Types: Keeping the default value system with no null because I think it works, but I also want to put more stuff into distinguishing mutability and to default to immutability beyond the first assignment.
OOP/functional: going to go to the full OOP side, which I know will make some people go "eeeeeehhhhh" but dw because I'm going to try and do things to make everything feel much more functional at the method level, such as:

Show thread

Single-expression method bodies are an explicit return, so you can do this:
fn sqr( _n n ) { n * n ; }
(as a side note: because of the contracts I'm adding, function *declaration* bodies need braces, but not invocations)

Pipes: Not sure about these. At the moment, the pipeline does 2 completely separate things: (1) if one part of a pipe expression is just a method call, then that call can take in a piped parameter and (2) each pipe section is just an expression in terms of 'pipe'.

Show thread

Thinking of it like that, it makes more sense if the pipeline is reserved *only* for the first situation and we create different syntax for the second, which really is just a hacky-looking way of mashing several statements into 1 - so really it would be totally fine to just drop the 2nd case.

The caveat is then you want to encourage writing methods that use pipes, but at the same time in OOP these would all be extension methods, so I'm at that place again where I could totally just drop pipes completely.

Show thread

Ok, a 'give' keyword makes sense for getting a value mid-expression for {} blocks.

I could make the braces mandatory to aid readability, but at the same time mean that single-statement blocks don't need the semicolon, so you wouldn't need to see { ... ; } which does look pretty nasty.

I'm thinking of also stealing range types, but at the same time I think there's a better way of doing it, like treating comparison operators as ternary, so you can do 0 <= x <= 10.

Show thread

The idea then being this thing with range types:
int int<0..100>.somefn() { ... }
becomes this with a contract that could get messy if used a lot but at the same time is more consistent with everything else:
int int.somefn() requires 0 <= this <= 100 { ... }

I'm a bit on the fence about this though, as range types do look fairly nice. It's in a battle of brevity vs consistency, and I think consistency is going to be more important.

Show thread

However, in some cases you can have both. Try this for a contract on an attribute:

int x >= 0 ;
int y >= 0 , y <= 100 ;

The good: really fucking brief because you just get the first thing in the expression for the variable and then the whole list as the contracts.
The bad: limited expressions because it has to start with the variable name.
The alternative: 'requires' annotation like methods have:
int x requires 0 <= x <= 100 ;

Show thread

The thing I was reading yesterday was talking about attribute contracts being on every single method entry and exit, but that could be trivially optimised by applying the contracts to the write operations of all mentioned mutable values in the contract expressions.

Show thread

Last thing I really need to sort out (that I can think of right now): types of bodies.

At the moment I have block<t> which is a non-parametric sub-scope, and fnbody<t> which is a parametric isolated scope. What I need is a parametric sub-scope i.e. what you'd need for 'foreach' for it to be able to define 'each' for you and for you to still be able to return from the encompassing method normally. This third type would behave basically like a lambda, just not being a lambda but a sort of pseudo-fnbody.

Show thread
Show more

@OTheB the problem with V is not that it's memey, but that it's amateur-hour bad from a person that is capable and should know better

also, its error system is just nicked from Rust

HOWEVER, I do come bearing good news, found this utterly massive article on the topic on error-handling, it's really a jewel

be warned, it's a 2 hour article

it also treats a bit of the more obscure stuff, like Eiffel contracts, or Clu handling

@trickster I'm about a 5th through and this shit's really interesting.

@OTheB it gets meandery in a couple of places, but I can guarantee that it's good from top to bottom

@trickster Right holy shit that was worth reading. I feel kind of pleased I was thinking along the right tracks with my original error/exception distinction, though it would've worked very differently to the exception/abandonment pattern. I'm also pleased I had the right idea about declaration conditions, but having both pre- and postconditions was new to me. Then aborts would be *incredibly* useful too.

I'm not so convinced on the Result<T> thing though, so I don't think that'll show up in O.

Sign in to participate in the conversation
Mastodon for Tech Folks

The social network of the future: No ads, no corporate surveillance, ethical design, and decentralization! Own your data with Mastodon!