print("hi, it’s talktalk")
We’ve got structs, strings, ints, floats, enums,
effects, bools, tuples,
arrays, chars,
records, and
functions.
struct Person {
let name: String
let age: Int
}
"hello there"
1000
1.23
enum Unit {
case yard(Int)
case meter(Int)
}
effect 'log<T: Showable>(t: T) -> T
#handle 'log { t in
print("t: " + t.show())
'continue t
}
true
false
(123, true, "hello")
[1, 2, 3]
'a'
'\n'
'🦉'
{ a: 123, b: "sup" }
func increment(x) { x + 1 }
increment(1)
struct Person { let name: String let age: Int }
talktalk is a programming language. It kind of looks like Swift or Rust, especially if you don’t know those languages.
syntax
Here, have some math.
ok ok, that was exciting, let’s write a function now
func add(x, y) { x + y }
Let's call the function with Ints:
Now let's call it with Strings:
Wow functions are polymorphic. What a world!
We can also define functions with labeled params.
"Ok Alonzo Church" you say, "but do you have like, normal variables?"
We do! I'm getting to it...
types
Ok Philip Wadler, maybe you like types? You can specify them if you want.
let a: Int = 1 let b: Float = 2.0
They’ll be checked.
But you can also not specify them and types will still be checked:
Functions can have type annotations as well.
// it's good to be explicit sometimes func identity<T>(x: T) -> T { x }
Functions are values too, and they can capture state.
You can also use trailing blocks for callback-y stuff.
objects
Ok Alan Kay, maybe you like objects. You know, big bags of state and behavior that are the only correct way to program.
By default, structs get constructors generated automatically. But if your struct is special then you can define a custom constructor with init.
Ok Chewbacca, maybe you're not one for all this ceremony. You can also just define records.
enums / pattern matching
What about enumerations? With attached values even?
You can pattern match on enums. Your match expression will be checked for exhaustivity.
We can pattern match in conditionals too.
Records can be pattern matched too.
Wow. Astounding.. Amazing. Simply... Extraordinary ok fine all modern languages can do that but talktalk can as well is all I'm saying.
You've also got a couple builtin enums, Optional and Result. They look like this:
enum Optional<T> { case some(T) case none } enum Result<T, E> { case ok(T) case error(E) }
Pretty standard stuff. I never promised you flowers. Or wait, i did? Ugh ok here have guard clauses instead, in the form of let else.
Speaking of bailing early, any two-variant enum can short circuit a function (say like, Optional or Result). Think rust's ? operator but dumber but simpler. Elegant? One might say. But one might say a lot of things so who knows.
What if you want to just go nuts and damn the torpedoes I know this thing is fine, stop yelling at me compiler, do you even know who my dad is?? For those cases you can use !, which simply unwraps the first variant and panics if it hits the second variant.
protocols
visits Glasgow once what about traits type classes interfaces protocols? For making ad-hoc polymorphism less ad-hoc? Yea we've got those.
Let's write a super basic protocol that lets a type be added to itself.
protocol Addable { func add(to other: Self) -> Self }
How do we make types conform to it? With a lil extend declaration. Think rust's impl Y for X or swift's extension X: Y.
Protocols can also have associated types with their own constraints (basically the associated values must conform to other prototypes).
You can even extend a protocol itself, handing a new method to every conformer at once.
extend Addable { func quadruple() -> Self { self.add(to: self).add(to: self.add(to: self)) } }
gadts
Not only can we pronounce "GADTs"1, we can use them.
Each arm of the match knows what T actually is, so eval can give you back Int or String, not T. Because everyone hates T2
effects
What are effects? Great question. I don't know. But i think they're like weird lil functions. Functions that can suspend execution, hand control off somewhere else, then return it. Think like, async/await in other languages, but more generalized.
Functions carry their effects as part of their signatures. In talktalk, panics are handled by the 'panic, I/O operations are handled by the 'io effect and memory allocations are handled by the 'alloc effect.
Hover your mouse over the function names to see their effects.
collections
Arrays. They do what you think.
Subscripts work the way you would think. Unless you don't think they'd work. In which case they work but they don't work the way you don't think they don't work.
There's some generic iteration helpers. It's not all the way fleshed out yet. It will be.
Ranges exist too.
There are tuples, with positional access.
And the stdlib has a growable string-keyed Dict.
strings
Strings are unicode-correct3. Iteration is by user-perceived character (extended grapheme clusters, UAX #29, etc. etc.), which means emoji can't tear.
Looping gives you one Character at a time.
Ok Werner Buchholz, you want the bytes? You can call utf8() to get bytes.
ownership
talktalk has memory semantics. what are they? um, basically: everything has value semantics, sharing is implicit and cheap (refcounted, copy-on-write), and the compiler figures out the retains and releases. You mostly don't have to think about it.
Mutation happens through mut funcs, which get exclusive access to self.
struct BankAccount { let balance: Int mut func deposit(amount: Int) { self.balance = self.balance + amount } }
Function parameters borrow by default, so calling a function gives nothing up.
But sometimes a value really is one of a kind: a ticket, a token, a file handle. Mark the type 'linear and it must be consumed exactly once. Not zero times, not two times.
Some day I'll tell you all about consume parameters, the Copy/Clone marker protocols, Deinit destructors, and exclusive &mut loans, but not today. I simply don't remember how they work atm.
macros
Talk has hygienic macros. The declarative kind is a token template:
They can introduce control flow, which functions can't.
Macro names don't capture variables, and expansions get type-checked like any other code.
The stdlib ships an HTML generation macro, written in talk itself, that parses and checks your markup at compile time:
Interpolations get escaped, and @for/@if live right there in the markup. This one isn't runnable in the browser (the playground doesn't run procedural macros yet), but it works from the CLI.
Here are some goals:
Learning stuff
This is by far the biggest goal. I didn’t super understand all the ins and outs of compilers. I still don’t but at least I have a way to learn now. You shouldn't use talktalk. But you might enjoy perusing talktalk.
Fully typed everything
Types are cool.
As much type inference as possible
I don’t know if it's a good idea. It’s probably not. I just think it’s neat.
Familiar-ish syntax
Haskell/ML-y syntax is beautiful. I hate it.
Figure out nice syntax highlighting color schemes
I feel like making full programming language is the only way to do this, right? Right? Don't answer that.
Here are some non-goals:
Blazingly fast performance?
I mean I’m probably not gonna litter the codebase with sleeps but I’m allowed to if I want.
Trying to make everything perfectly sound and decidable?
Is this even possible? I feel like I saw a YouTube video that said it’s not.
Trying to get others to use it?
Why? PHP exists, you should probably use that.
--
How it works1
First we lex the code into tokens. Then we parse the tokens into an AST. Then we resolve the names in the AST. Then we type check the AST and produce a TypedAST. Then we lower the TypedAST into our own lil IR. Then our lil IR is interpreted by a lil interpreter. There’s no trick to it, it’s just a simple trick!
At this point you’re cracking your knuckles saying "nice try, wise guy, but you’ll have to do better than that if you want me to adopt this at my fortune 500 company where I do a very good job and that is why they pay me the BIG bucks." Ok ok, call off your goons. Here’s some more detail2.
Lexing
It's a pretty standard lexer as far as lexers go. It takes a string and turns it into tokens. Tokens have start/end positions and types like StringLiteral, Comma and more.
Parsing
talktalk takes the tokens from lexing and uses a handwritten recursive descent parser (I think? I'm not good at genres) with some chris pratt precedence for expressions. Nothing too exciting here. At the end of this we have an AST.
Name Resolving
At this point we have an AST with things that have names. Like functions, variables, nominals (structs/enums), methods, etc. Those names are all strings. Since different names can be different things at different types, we give every named thing a unique number so we can tell the difference. Those numbers are called Symbols. I don't know if that's the right term but ¯\_(ツ)_/¯. Concretely this means replacing all of the Name::Raw(string) names in the AST with Name::Resolved(symbol, string).
Side note: I looked into using De Brujin indices for this but I wasn't sure how to pronounce it so I didn't.
The name resolution pass also builds a strongly-connected-components graph that helps the type checker check things in an order that deals with mutual recursion.
Type inference/checking
This is where I've spent like 90% of my time. Turns out there's a lot of history there. Who knew? Anyways I read a lot of papers and the /r/ProgrammmingLanguages subreddit and every post on [thunderseethe]'s blog like fifty times. Here's what's going on in the type checker.
- We take the SCC graph built during name resolution and iterate through different each group of binders3. At the top level, we have
- Nominal declarations (
struct T {..}/enum T {..}) get defined in the type catalog, which is basically just a big ol' dictionary of dictionaries of dictionaries. It stores information like:- What nominals are defined?
- What methods do nominals those have?
- What conformances do they have? What are their associated types?
- What type parameters (generics) do they have?
- What properties do structs have?
- What variants do enums have?
- What effects exist?
- Top level functions.
- Nominal declarations (
to be continued
--
At this point you’re rubbing your hands together thinking “mamma mia! I’m about to make a million smackaroos using talktalk!” Well you probably are. But before you start firing up your local AI coding buddy you should know what’s still not there yet.
IO
You can print to stdout! But that is literally it. This seems like something that would be nice to have! But we don't have it yet.
A "Standard" library
I mean there isn't an unstandard library either so I'm not sure why I put that in quotes. Anyway, don't expect to see familiar things like hashmaps, fancy algorithms (sorry al gore), serialization tools, etc. Or maybe I have added some of those things and not updated this website yet!
Explicit mutation
Right now everything is mutable. It's chaos. The sky is crumbling and all I have is a looney tunes umbrella.
Mutable Arrays
I haven't implemented push on array yet. You're probably a functional programmer with a cool leather jacket and ripped jeans and a backwards baseball cap who thinks mutability is for cats and dogs but I just thought I'd mention it.
Concurrency
But concurrency isn't parallelism! So does this mean there's parallelism? No, it does not.
Visibility modifiers
Everything public, all the time baby.
Documentation
None.
So anyway, maybe you can see what I meant by "don't use this, peruse this."