Share your Programming Language Jam submissions here!

← Back to all posts
Sen—objectifying. in a good way.
aachh (7)

Hey, welcome to a small introduction to the Sen programming language, our (mine and @sjchmiela ’s) entry to the jam. Please read this sad disclaimer before proceeding though:

Sen is sadly in a completely unusable state at the time of its submission, as such I haven't attached any repl. If you want to see the source code up to this point (yes, i know it's very messy) it's here. I've been developing it in a team of two only and that combined with the fact I'm going to high school in less than a week soon and I was repainting my room half of the time the jam was happening I haven't had a lot of time to work on it (my teammate was unfortunately equally busy). That doesn't change the fact the language is defined, something resembling a feature board is done, and that we’re looking to implementing all the features (and more!) described in this README. I know submitting a completely unprepared project is immature and the project might as well just be a hello world, but we’ve already written (at least started writing) a tokenizer, single-pass compiler (parser + type-checker), and virtual machine. It's just that most of the syntax simply isn't implemented yet. We’re going to continue working on this after the jam, even if we don't win or get any kind of prize, so that's that. We’ve seen what other teams managed to create in this short span of half a month (big kudos to all the other teams! you're all amazing!) and despite that we hope you can see the potential in Sen's design alone rather than the poor spaghetti code we've managed to scratch up in between other things to do. Thanks for reading this, we hope you like this little introduction to the language. :D

Now onto the actual thing, if you're still even interested:


sen

objectifying. in a good way.

Sen is an object-oriented, functional programming-nudged data representation language. How can it even be interesting if it's a data representation language, you ask?

Well, cause it kinda is one, but it also isn't. Cause it's a lot more than that. Check this snippet out:

myName = "Antoni"
myAge = 15
someObject = object {
  aProperty = "a string value"
}

Looks pretty similar to this JSON:

{
  "myName": "Antoni",
  "myAge": 15,
  "someObject": {
    "aProperty": "a string value"
  } 
}

Right?

Sure it's very similar. To the point that the Sen compiler has a command (at least it's planned!), sen json, to convert the previous code to the latter, which makes it perfect for working with and generating data. Sen is in its core a data representation language — each file is an object that is manipulated on in the virtual machine, which means… You guessed it! Each global variable is a field of that object. Pretty akin to JSON!

So, let's dig deeper. What makes us have the audacity to say this language even may be interesting for you?

Cause it's more than just data. Sen is about the transformation of data and its manipulation; giving it meaning.

Let's see what I mean.

Sen supports the concept of functions, which are pretty similar to those you may encounter in other languages, but also fundamentally different. Let's see an example, cause someone once said a code snippet is worth a thousand CS papers. Let us construct a function that will take a number and transform it to its factorial.

This is how it could look like in Sen:

function factorial for number with accumulator {
  leave accumulator if number == 1
  else repeat for number - 1 with accumulator * number
}

Now we're talking code! Let's dissect what's happening here.

Line 1 is the definition of the function. Functions aren't assigned to fields, later I'll tell you why. What's the for number with accumulator business though? That's how parameters are defined for functions in Sen, because you see, every function, transformation, needs to have a "transformatee," something to act upon, not with. That is defined with the for T construct after function. You can put any arbitrary type and then use its name as the value of the parameter inside the function. That kinda makes the one main parameter typed, the rest have no other way of being. It's typed because most of the time when writing Sen you won't be preparing functions for numbers or strings but for your own types. Then during compilation your code is being checked for accessing fields that aren't there, for example. The stuff after with are the additional parameters that control how the first one is transformed. They aren't typed.

If you have trouble grasping the "main parameter" concept, you can think of it like methods. Think of it like number is a class, and factorial is a method of that class. This isn't the right or original way to think about this though, it's only a helper. Functions are like tunnels, data comes in, altered-in-a-specific-way data comes out. Data isn't "alive" in contrast to objects constructed from classes. Data (and objects as you'll soon see) is just data, information that can be interpreted. It doesn't have a lifetime, it doesn't have any meaning until you give it one.

Line 2 is the "base case" for this "recursion." If the main parameter, the number, is equal to 1, we leave (not return! you leave the transformed parameter, it didn't go anywhere :P) accumulator. Else, we repeat for number minus 1 and accumulator times number in place of the previous one. Yep, this is accumulative recursion! Not really "recursion" though. Notice how in line 3 we use the repeat keyword. This means that we repeat the thing we did before, not recur. Sure, we may not reach an end if the base case wasn't provided properly, but other than that it's not really nesting anything. The Sen VM doesn't have any concept of recursion as e.g. Python knows it, "returning the function being defined." It's just repeating things until we reach some kind of condition.

How would we "call" this function though? Easy:

result = 5 :> factorial(1)

The :> operator is called the transformation operator — what comes before it is transformed with the list of functions that come after it. Note I said "list." Cause yeah; what if we wanted to calculate the factorial of 5, and then calculate the factorial of that?

Easy as well:

result = 5 :> factorial(1), factorial(1)

You can put any arbitrary number of functions after the transformation operator, and the result of the previous one will be delegated to the next one. Note this property: all transformations must take the same parameter. Therefore, a function that works for number will also result in a number. This is aid towards weirdly structured code in which when you take a parameter the result is of a different type than it(??? smh). There are no exceptions towards this rule. If you're thinking, "well, how about number-to-string or string-to-number parsing functions? Don't those break that rule?"

This is where we come at a crossroads. Like it or not, Sen is a data representation language, but a fresh one. It tries to have as consistent rules as it can, because language design inconsistency is what pays for unreadability, constant issues and rising in the "dreaded languages" category in Stack Overflow's survey every year. We think that it's better to provide a language with a smaller, tighter feature set, but a more consistent, reliable, understandable, intuitive and user-friendly design.

Okay, so how about some more interesting stuff?

Since Sen is all about the data, there should be a way to structure it, right? I bet you were weirded out by the object when I was comparing the first snippet to the JSON one. It's because object is a constructable type, meaning you can — have to — provide some kind of values when creating it. object is a special constructable type though because the values aren't specified. All constructable types you define are just typed remixes of object. They all die after compile-time, meaning you get the type safety, and the VM gets a lightweight, guaranteed-to-be-correct chunk. Okay, let us see an example already. Let's create a type that will bundle data representing a person.

type person {
  name: string,
  age: number,
  location: string
}

Now we can create objects structured like this and transform them! Awesome!

me = person {
  name = "Antoni"
  age = 15
  location = "Poland"
}

If you forgot to include a field or assigned a right-hand-expression of the wrong type, a compile-time error would occur.

Let's make some functions that will transform our people, shall we?

function age for person with step {
  leave person but {
    age = age + step
  }
}

function rename for person with newName {
  leave person but {
    name = newName
  }
}

function relocate for person with newLocation {
  leave person but {
    location = newLocation
  }
}

Now I can finally live a bit:

me :> age(10), rename("Anthony"), relocate("Sunny San Francisco")

The but keyword is used to indicate that we want to leave the object as is and mutate only some fields. If we didn't put that keyword there, the compiler would think we want to leave a completely new person object.

On that note…

Objects and their fields are immutable in Sen in the meaning that you can't do this:

me = person {
  name = "A different name!"
  age = 99
  location = "Completely new! :0"
}

Once you defined me. The same applies for number or string fields. The only way you can change values of fields is in transformations; reminiscent of FP-style immutability. This is because Sen's golden rule is to not change data in ambiguous ways. If you're up to mutate it you should know how to and through what. That's why in order to mutate something you need to transform the data. Transformation differs from mutation from the incentive; the former is structured and implies some kind of meaning while the latter is ambiguous and doesn't provide any context.

Sen doesn't have a for or while loop constructs because conditional function repeatability and main and side parameters make for some promising constructs on their own. Here is Sen code that prints "Hello for the <number>th time!" 10 times:

counter = 0
function loop for number {
  if number == 10 {
    leave number
  }
  log "Hello for the ", number + 1, "th time!"
  repeat for number + 1
}
counter :> loop

If a function doesn't take any side parameters, it doesn't have additional parentheses.


Keep in mind this is all rooted in the fact that this is just data, and therefore Sen has a couple of gotchas you probably wouldn't know about if I hadn't told you. Here they are:

  • Right-hand expressions aren't allowed by themselves. For example, in JavaScript you can do:
var five = 5;
five;

And that will print 5. Sen doesn't work this way because it doesn't allow right-hand expressions on their own. Why? Because it's data without meaning. It doesn't mean anything, it doesn't benefit anything: it's invalid. It's like saying "the sky" in the middle of a conversation. What can I even mean by that? That it's pretty? That it looks like it's going to rain? That I can see a funny-looking cloud in it? No context is provided and therefore you can't make out what I mean.

  • Sen's concept of scope is pretty different from that in other languages. Take this code:
x = 5
function printX for number {
  log x
  leave
}
function mutate for number {
  leave number + 10
}
x :> printX, mutate, printX

What do you think it will print?

…nothing. It's erroneous code, a compile-time error would occur and the chunk wouldn't go on to run. Why though? Sen doesn't allow you to capture values of other fields in functions. Functions should be able to transform what they're supposed to with what they get — the side parameters. This is why they aren't saved in fields: fields assume some kind of context in on itself, some kind of atomic non-contextual value. Functions only dance to what you play to them. They aren't independent and therefore aren't saved in fields, same as types. Functions aren't "reusable code" as in other languages. They're just a way to transform data because — once again — Sen is just data. You transform it. *cue epic blockbuster music*

  • Akin to the previous point, you can't create types and functions nor can you transform in the value-providing blocks when creating objects. For example this is wrong:
anObject = object {
  aField = "some value"
  function erroneousFunction for string {
    log string
    leave
  }
  aField :> erroneousFunction
}

This may be weirding out at the beginning too, I'm not judging you, but when you think about it it makes sense. A lot of things would be complicated if this would be allowed. Think about functions as something completely out of the concept of scope. Different names for them were considered at one point during development; including messages, methods, and transformations. Functions prevailed because they resemble what they do most, plus who wants to write transformation each time they want to define it?
You've got three entities; fields, functions, and types. They all live in their own worlds. It's not similar to other languages; they don't intertwine because there's very little room for intertwining. Fields represent data, types organize fields, and functions transform fields that types organize. This is the essence of Sen in one sentence.


That's everything we wanted to show you. There are other features we had plans to implement but simply didn't have enough time. If we got to win… we would get on them as soon as possible. :) Even if we don't, we probably will because we think this is an interesting project and are making it because of that alone.

In case of any questions you can reach out to one of us via Discord: ache of head#5837. We hope you see the potential in Sen as a language and as an accessible tool for everyone. Cheers! :D