A Programming Language for Games, talk #2
ฝัง
- เผยแพร่เมื่อ 21 พ.ย. 2024
- Second talk on designing a programming language for games. Given on September 25, 2014.
The first talk in this series is here: • Ideas about a new prog...
This talk quotes from an email by John Carmack that is reproduced here in full: number-none.com...
Casey Muratori's essay on Semantic Compression: mollyrocket.com...
Mike Acton's cppcon keynote is here:
• CppCon 2014: Mike Acto...
Happy 10 year anniversary of this classic
Gotta love that hard keystroke on slide change, as it expresses such resolve on the matter.
I never realized how massive the gap is between "mainstream" programming and game programming. It seems that "mainstream" programming is centered around the client-server architecture, whereas game programming is focused on a high-performance single-machine architecture. Servers have to be reliable and maintainable. Games do not. They ship once and maybe get patched occasionally, but they aren't meant to run constantly like a server is. If a server needs better performance, you have the option of slapping in more processors. For a game, you can't expect your players to slap in another CPU or GPU. The development cycle for a game is a few years, whereas a server might be developed, maintained, and extended for decades.
Since almost everything these days is in a client/server architecture, it naturally follows that the programming languages are evolving to meet the needs of those applications. And so you get safety, garbage collection, and whatever else. GC is a lifesaver for servers, as you will never have to worry about memory leaks that crash the server periodically. It's a huge problem for games, because you can't afford to have random slowdowns when the GC decides to do its thing.
Different domains. Different applications. Different programming languages. Perhaps Bjarne Stroustrup was wrong in creating a "multi-purpose" language. There really is no "one size fits all" programming language, and there never will be. So we need to stop trying to make one.
> [with GC] you will never have to worry about memory leaks that crash the server periodically
You might have to worry less about that, but you certainly still get memory leaks. Just not the kind where nothing has a reference to your object. You can absolutely build up a bunch of garbage because your program isn't properly unlinking references to that garbage.
With GC you have to start worrying about when the GC misbehaves and through song and dance trying to make sure your program performs well in spite of it if you have some kind of memory load. Have had times where GC freezes server so long external monitors think the service has crashed/gotten in a bad state and kill it for example. Basically I think you trade one set of issues for another set of issues.
iOS is uses a garbage collector (Automatic Reference Counting) and imo it's the smoothest, most responsive UX around. Plus it's been running on mobile chips for almost 20 years. I never noticed hitches or microstutters. Perhaps these performance problems are Java-specific or framework-specific? I definitely don't think GC is all bad.
And yes, when programming with the iOS GC, there's still the possibility of memory leaks but it's pretty easy to learn when those leaks can occur (when there's a reference cycle) and how to avoid them (breaking the cycle, usually by using a __weak reference)
99% of the time you just don't have to think about memory. I think it's pretty good.
Also, I don't follow this stuff anymore, but iOS used to be pretty famous for keeping way more apps open in the background than Android even though the RAM chips on the iPhones were super small.
Even though both Android and iOS used a GC, they performed totally differently when it came to RAM efficiency.
I think this also demonstrates that the existence of GC is not fundamentally the performance bottleneck on Android, and great memory efficiency can be achieved even with a garbage collector.
Q&A
1:16:22 Why not use contracts instead of captures?
1:17:14 Chandler Carruth, "Zero-Cost Abstractions and Future Directions for Modern Optimizing Compilers", LLVM Developers' Meeting talk, 2012.
1:18:28 Do you think that the code maturation cycle could be applied to memory and type, e.g. GC until you specify how you control the memory?
1:20:04 How to make memory management errors easier to fix?
1:20:20 How would templates, generics and function overloading work given this syntax?
1:21:49 Thoughts on controlled memory management such as arenas?
1:22:31 Would you have built-in types like Vector3 and Matrix4?
1:22:53 (I think coroutines are super-useful.)
1:23:20 Thoughts on namespaces and modules?
1:25:05 What would be included in the standard library given this proposed language is for games?
1:26:15 (My datetime module in D was a single 30,000-line file.)
1:26:31 "My biggest function in the Witness is like 14,000 lines, I think."
1:26:43 How do captures differ from call-by-reference and restricting scope access? (I think that captures should be function arguments.)
1:28:15 Have you given thought to using compile-time interpretation of code to generate generics?
1:28:30 Were named parameters discussed in this talk?
1:28:35 Should the compiler make available the memory layouts of data structures (e.g. to allow tighter game tooling)?
1:29:50 [Closing remarks]
Oh gosh, I agree with almost every single thing you've said in this video and the previous one. I'm so glad someone else is working on these things!
Just watched the first video and now this one and I find the talks fascinating, but I really like how much authority and force Jonathan uses to press a key to move to the next slide :).
I think it's really good to discuss programming language features in the context of what is good for games and how the balance point of cost/benefit tradeoffs might be different here.
There is a real problem of "always do it like this" and "best practice" in software development wisdom. Things always depend and true wisdom is knowing what things depend on for your situation.
Programming language design is tricky because you ideally want to enable programmers to decide their own tradeoffs (so your language is useful) and yet you want to make "right things easy" so you can't be entirely unopinionated.
Your openness and sincerity are very inspiring.
Please contiune posting videos and inspiring people. Keep being awesome :)
These are some really interesting thoughts Jonathan. I'm really sold on this new language your envisioning.
Keep the talks comming. (^:
you're not your.
@@yotty97 I know I am five years late but still want to remind you that people such as yourself are a waste of all of our time including your own. Grammar nazis almost always totally miss the point of what people are saying trying to nitpick the stupid insignificant stuff so do yourself a favor and stop stressing you're , your or the proper usage of there because you are doing no one a favor. If you can understand what someone means in a clear enough fashion nitpicking what someone typed in a comment off the top of their head is dumber then any misuse or misspelling of a word.
@@seditt5146 I didn't bother to read what you wrote but im sure it's dumb
@@yotty97 Yeah neat, not only are you a picky dumb ass but you are obviously a liar as well, not shocked at all really.
@@yotty97 Just take the advice that no one likes or is impressed by your grammar Nazi skills and move on my man.
Very interesting and enjoyable. Looking forward to the rest of the talks. Thank you.
Thanks once again for this - it's great unlearning the stuff I intuitively felt to be bullshit but had been programmed (sic) to do by default. Learning more by unlearning.
I agree with John Carmack when it comes to functions simply being as lengthy as required for whatever is supposed to happen in a sequential fashion. Honestly, I never understood why people need their code within two full windows, because while that might seem comfortable for programming itself it is of literal no usage to the game itself.
Your points about 'the joy of programming' are valid. They are paralleled in Jony Ive's philosophy - a drive toward simplicity typifying good product design.
Great Talk Jonathan. I totally agree regarding calling of functions. This is one thing that really grinds my gears as a project grows in size.
John Carmack writes really well, not only code.
I know this comment is 5 years old, but can someone point me to the letters written by John Carmack? I would greatly appreciate it!
Everything John Carmack does is fascinating. I could listen to him talk to me about shit I don't even understand all day.
@@Maraus92 number-none blog of John has a copy
I love sean's declaration syntax. It is exactly the same mathematicians use.
Being against the "many small functions" approach kind of took me by surprise, but I find myself agreeing. One thing I've found since taking this approach is that I have to decide between having complicated functional calls to pass mutable variables or moving variables to the outer scope. These are both really bad. In C, if you have a function with lots of mutable variables and you decide to split it up into 2 functions, you will end up either passing lots of pointers (and then dereferencing a bunch of pointers) or having a long list of variables in the outer scope which are now exposed to the entire file.
When you think about it, syntax is really everything. If it isn't important, then why are there so many different languages? The syntax is an extrapolation of the thought process, and so it constrains your thinking. I've been an ardent C programmer my entire life. I used to program in C++, but switched to C, because I just can't stand object-oriented programming anymore, and I don't think that it is the best direction to go. After trying Haskell earlier this year, my mind was blown once I grasped how incredible functional languages are. You see, I was one of those imperative programmers that just saw lisp as some bizarre language that couldn't possibly offer anything new. How wrong I was. It's not just that better languages are more concise, but that they offer a new way of thinking about problems, and a new way to express solutions that are easier to read, comprehend and maintain. Therefore it would not be a stretch to say that the syntax IS your language, and is hence your way of thinking.
But syntax is not the only thing that differs haskell from C++. You are basically saying that syntax is the most important thing that makes languages different which is wrong
I always define C++ lambdas at the top of the function and then call them at the bottom. This means that even when I capture everything by reference, the only thing that I end up capturing is the parent function's parameters.
Great talks, can't wait for the rest.
The capture block idea is awesome
Some good ideas here... definitely wish C++ had many of these features.
The section about declarations @ 29:00 is pretty much the way to do it in Delphi (~Object Pascal). 49:20 Syntax is everything to me hence I program in Delphi because I have tried C++ and I found it an atrocious language to work in. Delphi is very enjoyable to me syntax-wise, has it's own issues BUT I like programming in Delphi MUCH better than C++ for reasons amongst others being the syntax.
I felt the function assignment syntax was a bit inconsistent at around 48:00. To be consistent with the variable assignment, the type really should come between the colon and the equals. Also I feel the parameter name is part of the "function value" not the "function type". So I think it should have been more like this:
// Declare and assign together
square : float -> float = x -> {
return x * x;
};
// Declare and assign separately
square2 : float -> float;
square2 = x -> {
return x * x;
};
// Takes function as parameter (f is the name of the parameter with function type)
some_func : (float -> float) -> void;
some_func = f -> {
// ...
};
// Call with previously defined functions
some_func(square);
some_func(square2);
// Call with anonymous function
// Parameter type is specified in declaration of some_func so no need to specify here
// Just like you don't specify the type of a numeric litteral when passing as an argument
some_func(x -> {
return x * x;
});
Really enjoying these talks! Cheers!
I mostly agree with this, but I think named variables are helpful instead of just relying on order.
If I have a function of 4 or 5 variables, having line up
f : (float, int, struct1, int, vector3) -> object1 = x, y, s, z, v -> { ... }
It's harder to parse what y or z is.
I don't really have an alternative to suggest, I just don't think you quite have it.
Indeed. I agree!
Robinson Farrar True! However, I think Daniel did have a point as well, there was something off with the type and assignment usage with functions compared to the data element shown before. Unless I misunderstood.
I agree that there's something missing, but seeing a variable with type ((float -> float), float) -> float would be annoying as hell to read. Maybe the aliasing for different parts of a type could be standard. For example if you had built-in tuples (unlikely that he'll do this?) you could have a : (float v, string n) = (10.2, "Jeremiah");
and then a.v would alias 10.2. That might make converting a temporary tuple to a struct annoying, though.
I may be 10 years late, but I disagree with your assessment about inconsistency.
someFunc := (x: int) -> int { ... }
// is to be seen as shorthand for
someFunc : (x: int) -> int = (x: int) -> int { ... }
The type is still between the colon and the equals, the former case simply infers it from the right side of the assignment, which ofc still includes the full type information.
As an up and coming game developer, I'm constantly feeling guilty for things like large functions and using globals. This video made me feel a lot better. ^_^
If globals were not nessisary, the singleton pattern wouldn't exist.
As much as I highly regard John Carmack's work and opinion. You have to be a very good developer to write a multi page function that is readable. Because my experience has taught that doing that usually just turns the code into a big mess.
Not really, that is only true if the control flow is really complicated in wich case you should try to simply it.
If you can't simplify it that is generally a sign you have to rethink the problem and break it in simpler subproblems.
Also breaking a function into smaller functions is only really bad when the functions mutate state, if they are pure functions there is no big problem in writing separate smaller functions as long as you don't abuse.
i love the analogies to the aerospace industry
My problem with locally scoped functions is if you have 5 people working on the project someone who might wanna change it's scope to global might not even realise it exists.
Regarding captures: There's no good reason to have to declare captures at all. The compiler can infer automatically which variables are being captured, and most languages that support local procedures and/or lambdas do it that way. C++ is an outlier.
That said, I do see the value of an optional capture as you specify it.
I don't think that he was suggesting it as a requirement. In fact, I think that he was suggesing that it shouldn't be required because it could make your development cycle much longer to have put them everywhere, but he still is asserting that captures have value more than just for lambdas; they should be allowable on every block, so that one could definativly know if a block used any state outside of its scope. Adding them before release, but after development is done gives one peace of mind that their understanding of state access is actually how state is accessed.
I agreed with everything he said in this and previous video.. except for the ‘capture’ bit. The usage proposed for captures (refactoring a local function out) doesn’t convince me.
Having Good error messages from the compiler would be enough imo. The other advantages can be achieved by just using visibility modifiers and arguments
.
My argument is The visibility of global state should be handled by their respective owner. Not each individual function.
@@roxferesr I think this is an example of "big idea language" vs "incremental straight-jacket" approach proposed in this series. Deciding the blast radius of global variables only via their visibility across compilation units assumes you must move your code around in order to accomplish this "perfect placement". This, according to the thesis in this series, reduces the joy of programming. The counter-proposal offers to solve the problem by moving the scope guard closer to where it's used.
This talk is advance, you need years of experience to really understand this, but if you just follow it it works too
I came to the same conclusion as they did, but by using a educational program called Construct ( thought it is very powerful ). Running on js engine, i noticed performance or rather frame time improvements the closer I was to that structure that is being described in the video and in the end landed on it.
The problems with function definitions like this:
square := (x: float) -> float { return x*x }
Is that it's more difficult to gues the meaning for someone that does not know the language. It might be better to use:
square := function (x: float) -> float { return x*x }
One of the reasons why python is easier to understand is that it often uses understandable keywords instead of cryptic symbols. Short understandable keywords might sometimes/often be a better alternative than cryptic symbols. It's often so easy to understand that a non python programmer can get the gist of what the code is trying to do.
I really like your ideas on making the syntax for closures and functions friendly for refactoring, and would love it they get implemented in Rust. Have you completely given up on writing an RFC and pushing an implementation through?
I wish you good luck, Jonathan
Another great talk, I'm eager to hear more on this. Your syntax examples seem very appealing, not just for games. Raw pointers give me wet dreams.
One thing that I would be interested in. In the last talk you had joint memory allocation on arrays. As I understand it, arrays are very important for game development, partly because of Vector Processing as is done on graphics cards and other cpu vector extensions. You need the data you're operating on to be tightly packed together so that you can process it 4, 8, 16 or more times as fast in the leaf code using these vector processors. But when you're dealing with your higher level code, perhaps you might want to just deal with an individual character in the game. With arrays, you have to go struct.playerhp[playerid] and struct.playerarmor[playerid] and when you pass around characters to work with, you have to do the same, passing both the struct and the index in the struct. Do you think there would be a use for a language construct that would allow you to say player = struct[playerid] then access it as player.playerhp, player.playerarmor, even though these are not adjacent in memory?
Just make a separate type for the player where all components would be pointers? E.g. player = foo(struct, id); player.playerhp->bar.
After 8 years, I just realized that he was doing this presentation in a some kind of office, maybe Thekla Inc office. For all this time, I thought he was in his house or something
"Code has a maturation cycle". I just recently got into programming (again(?)) and I am sorta at the beginning of everything and only use console outputs. For my project I built a "menu", so shown text, user can input a number and then next menu comes up. My first attempt was 40+ lines of code for outputting menutext and connecting the functions to be called depending on user input.
This had a problem: When adding a new menupoint, my function grew bigger, it got kinda messy and confusing to look at, changes to menupoints would take many steps so that at the end the change would run correctly (for example: replacing a menupoint from top to bottom would need a change in the user-input-function so that the correct function would be called etc.).
So I ended up working out a menusystem where each menupoint is an object holding the shown text and the function to be called if chosen.
TBH this "refactoring" took me longer than I expected it to, but it turns out to be a very "bugfree" way and it takes less time to code the menus afterwards.
How’s getting into programming (again) going?
As for the first part (One big or many small functions), I would like to point out, that languages with nested functions solve that problem. With the extra benefit (if nested functions see the outer scope), that you can avoid passing all relevant and common arguments down to them. The next best thing is to have your MajorFunction() along with the MinorFunction(1..N)() in a separate module, where you only export the MajorFunction() (the Minors are static (inline) in C lingo).
Many years ago I tried to use some other persons (student, probably) code, which was 1 program with 1 function (main()), which was around 10k lines of code. Guess what - I did not use it :)
So yes, actually nested functions are one of the things which make functional languages sexy. Pascal not so much, in spite of it having nested functions :)
I really like the capture idea. It also lets you declare a function is "pure" with [ ]
Oops, commented before you got there lol
Personal idea as to coding best practices:
If the code works, and it’s not a major pain in the ass to maintain, it’s good enough.
12:00 functional approach and good types solves the second point
How strong is the [[capture]] guarantee? If a function declared that way is passed another function as a parameter, it can't guarantee that function won't violate the guarantee since the guarantee is not part of the type definition.
Generic functions: multiple functions for different data-types could be assigned to one variable. Avoids object mess.
The comments about Rust at 39min aren't quite correct. (Though it's possible things were different when the video was made?) You *can* use exactly the same syntax to declare a function inline as you do to declare it in the global scope. Example: is.gd/upLvmI The lambda syntax with the vertical pipes comes in when you want to declare a closure that captures local variables. There are a few different ways to do that, both inferred and explicit, and there ends up being some upsides to distinguishing that from regular functions that just take regular arguments. Also note: the vertical pipe lambdas are allowed to take curly braces like regular functions do, just not required to.
22:59 - Actually you don''t have to use lambdas to make a local function in C++. You can use a locally-scoped struct with a static function:
void set_up_the_people()
{
struct Local
{
static void AddPerson(Array& people, const char* name)
{
auto character = new Character();
character->name = copy_string(name);
people.add(character);
}
};
Array people;
Local::AddPerson(people, "Mike");
Local::AddPerson(people, "Leon");
Local::AddPerson(people, "Josephine");
}
While you're at that you can add fields to the struct where you put your captured variables and instantiate the struct and call the now non-static method and here you have a closure (without heap allocation)
@@MarkoMikulicic True, though in my experience the MSVC debugger isn't very good at finding symbols for those local structs, so it can be hard to debug.
@@MarkoMikulicic At that point you are just manually writing what the compiler will generate from a lambda
As far as I understood you are not totally correct about Rust functions and lambdas. Rust has different syntax for lambdas, but of course you can declare local function with just function syntax. And reason for lambdas have different syntax is that they can take some variables from their context. (you'll get a compiler error if you try to borrow/move context variable from a local function with suggestion to use lambda)
@12:00 If the comment doesn't match the code , both the code and comment are wrong. If there are no comments , the code can never be wrong as it does exactly what you told it to do .
I only code really little stuff at the moment, like pong and finite state machines, then you dont have to worry about anything they are so small.
I really like the capture syntax, it's one of the few things i really like about the lambdas in c++, the construction makes a lot of sense for non-GC programs.
I also enjoy the extension that the capture becomes part of the block and not part of the functions declaration, but it makes me wonder, where will it be stored when constructing instances of the anonymous function? I'm thinking both mainly in the case of recursion.
The capture can't be part of the declaration as that memory is generally shared between multiple instances. This leaves us with "somewhere on the heap" which always makes a game programmer shudder. Or we can place it on the stack and somehow tie it to the function object, but then the function object no longer has a set size meaning that we have to handle variably sized types...
Has this been dealt with in any later videos and talks?
Max Danielsson Not yet. Lately I am thinking that the capture syntax discussed here is not heavyweight enough for what one might really want to do in terms of scope restriction. But I haven't actually worked on these ideas yet.
Jonathan Blow Ok, Understood. Thanks for taking the time to respond.
I really think your ideas are in the right directions regarding what the game development world needs, many of your ideas resonate with my own experience in building games. The potential for something like C in the control of memory but with some extra modern concepts that fits within the general agenda of "high level assembly" is always going to be useful for as long as we have to consider actual hardware.
That's why the capture idea is so interesting, it's so simple on the surface, and it seems like it might be a good fit, but there are potential complications that might migrate away from that performance control that we need, and if the language has too much of that, or even any, that we can't avoid, i believe many might categorize the language as being "too high level" or "scripty". We have to be able to visualize what a specific piece of programming means for the assembly, memory access patterns, etc and shape that to create programs that the computer likes.
C++ is such a pain to me, i sometimes have to be so selective about its features depending on my goals for my architecture. I've made attempts to be more of a purist and build things in C, and in some ways that "feels" more right and clean, but there are some things in C++ that i start to miss that really helps with productivity. The C++ toolbox just simply is bigger, even if it does contain things like screwdrivers with razor blades on the handle, it still has that nice inner core of C that we all know and trust. There must be some middle ground or a new direction that makes more sense for our field.
There are so many things about this talk that are just awesome. Love it!
I would personally like it to be just slightly more "talky" instead of just symbols. That would increase readability and learnability aswell. Would also help syntax hilighting. Typing a few extra chars won't hurt.
The presentation by Mike Acton at CPPCON is posted on TH-cam at: CppCon 2014: Mike Acton "Data-Oriented Design and C++"
Seems like local function declarations will get you the bulk of what you're after. I think having to explicitly state all of the parameters every time you want to use a lambda sounds tiresome and prefer the C# way of having named delegate types which encapsulate the function signature in a single identifier. Not that C# then used that opportunity to make declaring anonymous methods easy, but they should have.
I guess that in 2 years we will get access to Jai.
What about lambdas with inferred return types. Some languages have lambda shortcuts (e.g. currying or #(...) in clojure).
Rust is somewhere in the middle, e.g. |x|x*2 is only 3 characters of 'bloat' to make an expression a lambda
in the rust community some people are still demanding further lambda sugar... but I think its' just right as it is now.
The other reason Rust has different syntax is: a lambda is physically different to a regular function, its got captured variables. part of rusts' philosophy is against 'hidden costs' in both memory & time.
I am staring at the rust Guide, and it is not obvious to me whether or not Rust could be extended to unify its functions and lambdas into one concept after 1.0. Jonathan Blow initially says he's unhappy that there are separate syntaxes for function-like parameters and captured parameters in Rust functions vs. lambdas, but the unified function/lambda he proposes also has separate syntax for the two types of parameters, so it's really the non-unification I think he's taking issue.
Nice talk, but I feel that what generally kills large projects, apart from changing requirements, is mostly flawed design and buggy implementations, not “friction”. Relaxing constraints on globals etc. is something which is primarily an option for very good, disciplined programmers, not for the programming populace in general.
Implicit types are a bad idea. Out of all the programming constructs I can think of, I can think of only one that is more dangerous - you don't always know what type a compiler is going to choose, and it gets really tricky really quickly. Although it may save a keystroke here and there, overall you're making your code more difficult to interpret by others, as they have to pursue the same interpretive logic that the compiler does, and ultimately you're writing less readable, less predictable code.
While I can see the point of it purely for mocking up something in a hurry, really you're not saving much time. Certainly if you want to use that function for anything larger in any future scenario, you want your types to be explicit, not implicit, for the reasons stated above.
Please note that scenarios such as declarations where type is specified by the type of the return value, are not true implicit type declarations (as the return type is declared explicitly) and templated functions are of course a completely different thing.
Lots of newer languages have a fmt or formatter that you can run your code through (even your text editor can run it) to format your code. I don't see why part of this formatting tool, which is using the same internal representation of your code as the compiler, couldn't add explicit types to all implicit definitions.
@@jaysistar2711 could or could not add?
@@nintendude794 Thanks! I fixed it.
1:24:00 ... I agree I really wish Rust kept elements "pub" by default, they used to have "pub" and "priv" keywords, but then on their minimalist drive decided to eliminate "priv", they did stats on their own compiler and made a judgement that everyone MUST code their way :(
safety critical fields could be marked priv (or even unsafe?), that would be better, IMO.
Interesting talk and topic, but your point with "const" is rather weak. Const has tons of benefits, and some languages like rust or D make it even more important with the additional level of constness, "immutable". Super important in any concurrent application.
If at some point you say "Damn, I need access to a non const X from a location where only const X is available!" it means your design has some flaw. Const is crucial design tool to enforce correctness by design.
I used to think the same way Jon does about const and I do still agree with him that repeatedly typing const everywhere is very tiresome. Having to type it twice on a single parameter in c++ is a bit nuts: "const int* const param". My own language requires lots of use of const in order for the automatic parallelism to work and it's one of my least favourite things about it. I did toy with making const the default and having to explicitly make things variable, but in the end it seemed like too big a departure from what people are used to. Now I'm starting to think I should re-consider.
I'm a fan of Rust with immutable default, you just mark what's mutable . Const feels annoying in C++ because they got the default wrong. EDIT: Also Rust propagates immutable better .
walter0bz I did read up on Rust a while back but I'll have another look, thanks. I don't like mutable as a keyword though - it's annoying to type :-)
Edit: I see Rust uses 'mut'.
walter0bz I agree with that. But it's not the const concept that is wrong in C++, const should be there, only flipped, thus removing the "const" keyword and adding a "mutable" keyword or something like that.
shobbs72 Sure it is annoying to type. The language want you to encourage to think about a design that don't need mutable.
12:00 Python has tests embedded in docstrings, which are basically comments that get run and tested. It's not really the most commonly used feature though.
26:00 Actually, the Test Driven Development books agree with you on this a bit. Write your test, then Implement it in the simplest possible way to pass the test, which will often be cut and paste solutions, then once the test passes you factor it to reduce duplication.
24:36 Quote: *now note that the syntax for the function declaration changed between when it was a lambda and when it was in global scope* Jonathan, I have read other people complain about this inconsistency before too. It didn't really bother me, but it begs an interesting question of "How do we design it to be more consistent ?" This is what I came up with so far...
I call the syntax *Function Builder Notation*
gist.github.com/Europia79/78bf804f7630a6a64ebf7cade1ce219e
class Main {
/**
* main() function that demonstrates the syntax of Function Builder Notation
*
* .returns(void) would be optional since it doesn't actually return anything
* .captures() is optional since it doesn't actually capture anything
* .throws() is optional since it doesn't actually throw any Exceptions
*
* This notation attempts to provide a consistent way to declare
* both global functions and locally scoped lambda functions.
*/
fp main = fn(String[] args).returns(int).captures().throws()
{
// fp = function pointer
fp lambda1 = fn() { }; // fn = pure function
fp lambda2 = fx() { }; // fx = impure function with side effects
fp lambda3 = fm() { }: // fm = function method with captured state
fp lambda4 = fm() { }; // automatically captures all local variables
fp lambda5 = fv() { }; // fv = virtual function with default behavior
fp lambda6 = fv(); // virtual function with no default behavior
return 0;
} // End of method main
// Example showing multiple return values
fp example1 = fv().returns(int, int);
// Example showing a function parameter & function return value
fp example2 = fv(fp.r param1).returns(fp.r);
} // End of class Main
49:10 Nice, just prefix process_array 's second argument with a some keyword like *fn*, *fun*, or *function* (for readability), and you're golden... There also seems to be typos: Should probably be
process_array := (array[] : float, x : (float)->float) { ... };
(since we're just defining process_array, not actually calling it yet... it's body would call x() and pass a float to it... and when we call process_array, we'd pass a function as the second argument... process_array itself seems to be a void function: Or in other words, an operation on the first parameter: An array).
Altho, I'd probably prefer
*fp* process_array = *fn* (float[] nums, *fn* .r x) { ... };
With body:
fp process_array = fn(float[] nums, fn.r operation).returns(void)
{
for (int index = 0; index < nums.length; index++) {
float n = nums[index];
nums[index] = operation(n);
}
};
fp no_op = fn(float f).returns(float)
{
// do nothing
// no operation
}
fp increment = fn(float f).returns(float)
{
return f + 1;
}
fp square = fn(float f).returns(float)
{
return f * f;
}
Calling it:
float[6] numbers = { 0, 1, 2, 3, 4, 5 };
process_array(numbers, no_op);
process_array(numbers, increment);
process_array(numbers, square);
process_array(numbers, fn(float f).r(float) { return f - 1; } );
51:26 capture
f := (x : float)->float [y] { return x * x + y };
fp f = fn(float x).returns(float).captures(y) { return x * x + y };
Sorry, I wasn't 100% sold on my idea of Function Builder Notation... so I wanted to refactor your method to C% notation & see them side by side.
Great readability... debatable writeability... I can see a lot of people complaining about the extra keystrokes... but I personally don't mind the extra second or two.
I'm actually loving the readability of it.
Anyways, eventho I'm not a fan of your specific notation, I do greatly appreciate the goal of consistency.
Awesome video !!!
58:10 Ah, yes, your syntax has awesome consistency for also being used for blocks:
Array people;
[&people] {
auto character = new Character();
character->name = copy_string("Larry");
people.add(character);
}
// In order for me to be consistent, mine would have be
captures(people) {
// ...
}
Yours is very nice & elegant... not sure how I feel about mine. It kinda looks like a function call. lol
1:08:55 Okay, I was doing *fn* to denote pure functions and *fx* to denote impure functions with side-effects...
Your double-bracket notation to denote pure functions is a nice idea.
One thing about lambda and function pointer: lambda AREN'T function without name. They are objects of an anonymous type with a call operator method (which can be template overloaded in C++ 14). std::function is a polymorphic container that allow to contain either a function pointer or a lambda (or even a method pointer) and present it with a common interface that any function can called.
BTW, lambda never allocate anything in the heap. It's a struct on the stack, whatever the closure's content is. If any allocation is done, it is done by the type of the closure members (like for string copy), but not by the lambda itself. In the case of std::function, it's its polymorphic nature that force it to use the heap (since it can contain objects of different sizes). Also, that's the big reason capture must be part of the type : the variable size is very different than one from a function pointer !
Now, it would be nice to have a common accessor that allow you to make non-capture lambda compatible with function pointer.
Edit: add note about the incompatibility between lambda and function pointers variables.
A non-capture lambda *is* compatible with a function pointer.
Mr. Blow you should make this language. Your celebrity status leaves you in a good position to leverage this change. Thx
He does and has done this for ~4 years now, why did you write this comment? :o
Yes, I'm 4 years too late, but I feel the need to point out that the `x` in `(x: float) -> float` seems totally unnecessary for the function *signature*, though it's obviously needed for the actual definition. I understand your argument for consistency, but I feel that throwing x into the function type just confuses things. I haven't seen JAI yet, though, but I hope that was pointed out before this.
I suppose you are right if coming at it from type theory... but giving some parameter a name in a function declaration goes a long way towards self-documenting the code, in a language designed for actually programming in it.
Of course that 'x' here doesn't add much... but consider: "makeCircle :: (radius: float) -> Circle".
That seems much, much clearer to me than "makeCircle :: (float) -> Circle", and much less a hassle than "makeCircleFromRadius :: (float) -> Circle".
Now, if using that function signature in some higher-order construct (like... 'apply a transform with arity-1 to all elements in a container'... err... "map"?) you indeed have a point.
But then, nothing really prevents such a language from *also* allowing "unnamed" parameters, as C++ functions declarations would also allow. In the case of Jai, though, maybe it could interfere with some of the polymorphic-types-stuff... and would jeopardize the now supported named-parameters-passing syntax (from caller site). Yet the function's signature in "map" could anyway define its own generic 'x' name (or none, if possible) ; and still match 'makeCircle' definition...
Oh, well.
m2c
Applying a capture to any block is the most important thing in either of these two talks IMO. Had in the back of my mind the whole time that the biggest benefit of functions is they make the dependencies of the lines inside explicit. "These three lines *rely on these two variables*, and *you don't need to keep track of any local variables after they finish*. Long functions suck because any line could theoretically rely on or affect any other line, that's `N!` complexity. A 400-line function - what is affecting what? That's way too complicated. How do you get around that? Local functions work, but they're very verbose and the meaning is ambiguous, being a function implies the code is supposed to be re-used.
But you pull each conceptual chunk into a block and use captures... Well, that's it. Solved.
The main problem with the education system is they teach the wrong things. I'm self-taught and learned to program by reading good and bad code. A better way of deciding when to externalize a chunk of code is to ask this question "is any other operation going to need this?" If the answer is no, then it's better to inline the code. If you find you have lots of code that does a similar thing with minor differences, you should take a step back to identify if refactoring is needed. Sometimes the answer is yes and sometimes it's no. Programming is an art.
universities focus too much on algorithms, theory and concept. University professors generally suck at writing great code that's maintainable. best way to learn how to write great code is to read great code. thankfully there are examples in open source, but you do have to find it. 95% of the code open and closed sucks!
For syntax reference regarding lambdas please check out Kotlin, I think they nailed it!
agree, but you are a little late
I disaggree on your point of putting the functions all together in one place. You say that this is "bad" because you now have to deal with multiple ideas when trying to understand the code and knowledge becomes implicit. I think it is actually THE OPPOSITE. You see when you move different pieces of functionality into there own NAMED function, you make your idea of what you are trying to do EXPLICIT and it shows how your original complex function was composed of smaller simpler pieces together and when reasoning about the code, you can do it in small simple pieces instead of a big chunk code that does something complex. Ofcourse this is not the only way of composing functionality nor the only way to enhace readability, it s often "good" to do so
I have to agree, the rationale there was weak. Moving the expression of an idea (i.e., into function definition) doesn't change the number of ideas, it just changes where the idea is expressed.
I get where he's coming from though. I've wrestled with the same problem writing scientific software, where the same sort of "programming school" truisms ring pretty hollow.
There is a parallel conversation here, about testing, lurking just out of sight. I'll leave that sleeping dog lie.
----
Ok, at 12:43 I see where he's going with this, and definitely agree with the specific point he's making there.
Actually i think you didn't understand what he means by implicit / explicit knowledge. The knowledge wasn't about the purpose of a piece of code but the other points he made. So it was about where is that method used, what state is a sub method expecting, ... If all your objection is about telling the reader what a certain piece of code does, you can simply plug in a line comment before that piece of code which will label the next section. The benefits are that you aren't constraint to a singleCorrectlyLabelledIdentifier but are free to describe the functionality more clearly than with a method name. It's easier to follow the code since you can read it top to bottom and not have to jump around to fully understand what the method does. Code should be written self-explanatory in the first place.
Apart from those points in time critical environments method calls can be slow. I've once written an image / texture rescaler in C# which does a simple bilinear sampling of the original texture. After inlining all method calls the code could be simplified by quite a bit. It was way better to avoid cache misses and run 8 times faster than the original code. Yes, it was a bit harder to understand (if you're doing several lerps manually inline it looks a bit confusing) but the overall length of the method was just a few lines longer but has everything embedded and was more simplified.
I was about to disagree at first too. But actually he makes a good point and, unlike John Carmack, doesn't actually discourage factoring your code that way at all. It's more that the public interface of your code should be as lean as possible and exposing your partial functionality is probably a bad idea. You might later learn that something initially meant to be only partial functionality actually makes sense to be more widely accessible but that should be a deliberate step, so you move it from being a closure to being a global function or some scope in between.
I don't think performance has to be an issue here. It's only about human perception. Ideally your compiler can automatically decide to inline the code, particularly with code that's only in a local scope, worst case you use a "inline" keyword to make that explicit.
what is the difference between a capture and an argument? By value vs by ref? If you're just reading a global variable, why couldn't you just pass it in as an argument?
Capture works like the member in a functor. functor is an object that overloads operator ( ) and has a state (which is stored in his members). In c++ when you define the lambda is like executing the constructor of the functor and passing initialisation for the members to it.
I find the on-going discussions about syntax somewhat grating, why is it we build nice visual scripting environments that reject invalid input but when it comes to our own tools for serious programming text is seen as the absolute height of sophistication in code representation =\
Having a single keystroke for each available action in a given context such as defining a new function or adding a loop or a assignment statement would surely increase the joy of programming, seems code editors could learn a thing from the UI's of games and 3d software.
Interesting, I'm reasonably confident that if you look at the file for those visual scripted uh... scripts, they would be in plain text(with some info about positioning maybe).
So what's to stop someone from building a C++ IDE that represents code this way?
Freelancerk1bbles Probably the sheer complexity of the C++ standard.
Freelancerk1bbles It's possible they are plain text but most likely they're formatted for easy machine parsing for the sake of keeping the editor simple. A C++ IDE that works this way could be made but it wouldn't benefit at all from keeping C++ syntax, it's only the semantics of C++ that would (potentially) be worth preserving.
***** I think the syntax should be simplified completely but for the sake of simplifying development environments, we should use source files that are easy for our tools to read so that our tools can be simpler. I see no use in an editor allowing you to write syntactically invalid code just to have your compiler tell you it's rubbish, so why not have a source code format that actually reflects the structure of the program rather than just a string of letters that every tool has to re-parse?
vimeo.com/36579366
Something like that?
***** What he describes is a node base visual programming environment. These things are really elegant solutions when it comes to solve a specific well defined problem set, to prototype small things or to describe logic in a declarative manner. I usually dislike them for a number of reasons.
In case the Mike Acton presentation video he references doesn't show up in your sidebar. CppCon 2014: Mike Acton "Data-Oriented Design and C++"
Do you have this available as a blog post instead of a video?
what about c# syntax?, my idea is making a native compiler for c# using roslyn & llvm, & throwing away the .NET dependency. we can also add features if needed.
Maybe a bit late by know. But at minute 46:18. Lambda return values, maybe I would put the returning type at the like} : float. It would be consistent with the rest of typings in the language, and would indicate you can skip the typing
Minute 52:58. By this time I think the one proposed in the video is better option
59:27: fantastic summary
Don't Modules solve 15:00? That and private methods / functions. The general premise does connect well to the misuse of OOP. I know that my intro to c++ classes taught the wrong way, to break up any function "longer than 5 lines", which inevitably leads to spaghetti code. This excessive focus on not repeating any logic, on code reuse, is disastrous, since it tends to split the logic over potentially multiple files, and makes it impossible to understand. A big function, that exposes a single interface, that hides all intermediate results, however ugly it may seem, is often a better solution. But with privacy, you can get reuse without allowing other code to depend on it, making it easier to refactor.
Though on your previous video struck very positively, I am concerned about the great multitude of additional "features" that are part of the language, rather than something that can be added above the language. Otherwise, people would need to spend enormous amounts of time getting used to this specific language, and its syntax, rather than just coding.
I guess my frustration was it feels like there's a lot of things that you want to be part of the language, that I think are things that should be above the language. While you should probably be able to let people make threads, coroutines, types of lambda's, etc... It's okay if they need support a library to do it.
CNLohr I disagree. I think these things need to be built in so that they can be made more efficient and both from a runtime perspective as well as a development time perspective. I don't want to have to manage support libraries or make sure I'm importing all the things I need when they are so fundamental to what I want a language to do. Also, if it's build directly into the language, we can get better error messages and debugging information since that will all be built right into the compiler.
CNLohr I agree with Casey. Syntax is a tradeoff: you put in the effort to learn the syntax for some language construct so that it's faster to do... whatever it is later on. If there's a lot of syntax around things you never use, that sucks, because you spent that effort and got little to nothing out of it. But if the syntax provides quick access to features you use all the time, it's a huge win. It pays for itself.
The domain of the language plays a huge role here. Jon thinks that threads, coroutines and lambdas should have syntax because he's designing the language around his use case. He uses them so often that it never really makes sense to *not* have them "imported."
A simple example: there's a programming language called Inform that was designed specifically for creating text adventures (like Zork). Inform has native syntax for representing a "room," a discrete area of physical space described by some flavor text. That would be an absolutely ridiculous syntactical construct in C, but it's essential in Inform, where it's an absolutely core use case. So Jon is designing a language around a set of use cases involving memory management and concurrency; if most of the people using this language will need lambdas (for instance) most of the time, that's a great reason for them to be in the language.
Both of you guys make good points. I do think lambdas, and the like need to have language features.
From Jon's later videos, I see more fully just why these things need to be language constructs... And why another syntactical approach isn't that good, i.e. wrapping everything in things that look like (or are) macros. So, that's fair.
I'm just always thinking about the lower end of things, trying to get more to bare-metal.
Did programming schools get worse since my only formal CS education in 1975? Back in the days when there was no OPP, no FP, no whatever, only "Structured Programming". I'm sure I recall that functions were presented as means of wrapping functionality that is used from multiple places in a program. That is to say for reuse, saving on duplicating code. There was no suggestion to split big code into functions for sake of keeping functions small for no clear reason.
I never had any issues with Global variables, PEEK, POKE and GOTO.
@@____uncompetative Ah... The glorious old times. To this day most of the code I write would be just fine if everything was global all the time.
Coming from the HPC world I can confirm how many "programming best practices" just obfuscate the task and sharply decrease performance.
If you are afraid of pointers, long functions and designing for performance maybe you should have chosen a different career.
7:00 // MinorFunction1(); isn't a function as it is inside the void MajorFunction() { body where MajorFunction() isn't a function either as MajorFunction is a potentially side effecting procedure and not a pure function free from side effects, and because all procedures are potentially side effecting it doesn't matter if MajorFunction() isn't because the code that makes use of MajorFunction() can't rely on MajorFunction() being free from side effects (without examining its source code, and understanding it enough to verify for themselves that it contains no side effects, and even then a change to MajorFunction() in a collaborative project could make it go from no side effects to side effects or even side effects in some rare conditional cases depending on certain input values), so the body having a sequence of imperative expressions/statements is not a function as you can't invoke these MinorFunctions in isolation from each other, whereas you can if they were factored out as separate procedures. At least in that latter pattern (the College style you refer to), these procedures can be independently invoked separate from each other and have well defined inputs and outputs, rather than some _ad hoc_ visibility of locals visible in MajorFunction() or globals visible in the main program. A pure function should only see its parameters and global constants, such as "magic numbers" which are best put into a global constant rather than repeated throughout the source even if their value is deemed constant this constant could change if the requirements end up changing when making a version for another platform with different architecture. Pure functions should not even see anything from the outer scope unless they are defined as inner functions that are textually nested within the other function, but even then the default should be that their own local variables and parameter names occlude visibility of those same names in the outer one. I'm not a huge fan of *this* being a way to refer to the variables in this outer scope, and there are ways to hack systems that rely on *this* being loosely defined, so it is best avoided even if it will require the inner function having to have more parameters which are the variables within the enclosing scope you want it to see which it might not see normally due to occlusion. Indeed, not being able to see anything of the outer function (as would be the case with a function if implemented outside of a nested textual inner function) would be more pragmatic as it would allow a function to start off as an inner function (like a genuine MinorFunction1() defined within MajorFunction() at the end of MajorFunction()'s body, being invoked at the start of MajorFunction()'s body along with the other two numbered minor functions you are decomposing functionality into), and then be refactored into a function outside of any function (and therefore visible and invokable from anywhere and consequently having more utility, which implies the only reason for nested functions is to hide complexity of an API as the "services" something needs in order to do its work could be hidden in its own definition and not usable by anything else, then there may be an argument to allow it for closures, but I think Jon said he doesn't use closures, and I have never needed one, and very few of the languages I have surveyed have had inner functions, so it is possible that a simpler syntax, with no closure semantics, would be preferable, and have more utility as it would force all functions out into the open where their services could be used, even if it wasn't anticipated they would be used independently of the MajorFunction that composed them as sub functions. Phew.
did you seriously think that i would read all this shit by you?
great videos
48:15
What do you mean with := type -> type {return...} & just = type...(exactly the same as the first), I think you're covering it... I dig the := can I; foo : type -> type = {...}
Well, the lambda syntax includes the type so you'd have to write it twice in that case, which is not ideal!
Hey, thanks for replying I have a massive amount to catch up on. Is there a repository for this? This feels like what I wanted of javascript.
Not at this time!
Could your language kick inversed defined statesets?
stuff like:
if (precise condition){
}
else { //do something} // here be only erroccases
or if (i != -1 )
were many always assume that nothing will change to the code, and assume all cases will remain correct.
Also syntactic sugar request :
if &&{ a==b;c==d;etc.} || { what== if; etc.}
I only have moderate experience with C++ and I'm not familiar with lamdas. I assume these are borrowed from lisp as Jonathan insinuated, but are these new in C++11 etc.? Also how do lamdas relate to a monad? (I'm not very familiar with functional programming either..)
1:09:00 indeed. That's pretty cool
When are you going to upload the next talk and where do you stream this? I would like to watch the next one live. Thanks
Typing arrows is annoying and I don't see why you need them.
I would much prefer another colon instead of that arrow and also wish the arguments were next to the function call e.g.
foo(x:Float, y:Float):Float = x+y
Lambdas can still use this (with underscore as the anonymous name)
(_(x:Float, y:Float):Float = x+y)
Hey Jon, I've been following you as a game designer since gears of war came out. Although I'm an artist, I'm trying learn programming. I took an introductory programming class relative to simple procedural programming in college before i graduated just to get some credits, I wanted to learn how to program for games, how can I be as proficient as you? Aside from syntax, where can I learn to program for games? Where can I learn the logic for coding games?
Did you end up learning to code in the end?
@@SpeedfreakUK Nope. xD
I went to a visual editor called Bolt instead and this worked out amazing. 😆
Seeing everything in code it too vague and monotonous for me. Its easy to get lost, easy to f***k up on the wrong or similar looking logic, and syntax on top of that is beyond annoying. I also don't come across videos that provide a sensible understanding of code logic and algorithms and as a result I almost never see progress; even when the logic seems correct, it always ends in frustration and a desire to give up. Using a visual editor, I hit the ground running in 30 mins. I was knocking out features like crazy. What should have worked in code, works perfectly fine in visual scripting. Now I have a prototype. Its still in development, but it shows promise. Unfortunately my 9 to 5 job cuts development time dramatically so I have to break things down to keep the ball rolling. Overall, it's looking alright. 👍
27:45 *declaration syntax* I'm not a big fan of this colon syntax that all these new languages are doing: Languages like Go, Rust, Kotlin, & Jai. This whole nefarious idea is rooted in SAVING KEYSTROKES !!! (via implicit types). However, I often find myself thinking in terms of 'building blocks'... so when I'm writing code, I think first & foremost in terms of "What do I need ?" (What type do I need). And also, when I go back and READ code... I find myself examining type information first. I don't know, maybe it's just because I'm so used to the syntax of the C-family of languages like C++, Java, & C#. But in order to placate all the keystroke saving warriors, I will suggest an alternative syntax:
First, the problem syntax:
HashMap pmap = new HashMap();
Bad example, I know... but notice that the problem is that you type HashMap twice.
Translated to Rust, Go, Kotlin, Jai, it would probably be something like
pmap := new HashMap(); // No view of the interface
// with a view of the interface:
pmap : Map = new HashMap();
I don't know about this... At first, I was thinking maybe prefix the interface to the implementation like
pmap = new Map:HashMap();
But this has the obvious problem when you declare without initializing.
Then I thought about a possible postfix &/or object-notation:
pmap = new HashMap().cast(Map);
I really like this object-notation syntax, especially when paired with short-hand notation for Number types: integer, float, decimal, etc... but unfortunately, it has the same problem when you declare without initializing.
Then I came full circle back to the original notation:
Map pmap = new HashMap();
Summary:
It has GOOD WRITEABILITY. (How we think when we code).
It has GOOD READABILITY. (Types are important... otherwise, we'd used a dynamically typed language).
To elaborate on readability... when I'm looking at a method signature, I don't really care what the parameter names are... I care what the types are. Put the types first... not the names first.
If you guys come up with a way to save keystrokes without sacrificing writeability or readability, then that's good... but the job of saving keystrokes is probably better left to IDEs.
Anyways, at least I think I came up with a better casting notation. Compare:
((SubType)object).dispatchMethod();
object.cast(SubType).dispatchMethod();
I would disagree that it's about saving keystrokes, although it's nice that it does save keystrokes. I think you've misunderstood its use case. It's supposed to be a replacement for *auto* in C++ or *var* C#, and only used when the type isn't necessary as long as it follows the correct semantics (i.e. duck-typing), or is redundant.
Now consider a more proper comparison. Which is cleaner?
auto x = new Vector(); // C++
var x = new Vector(); // C#
x := new Vector();
It's obvious that the type is Vector, so we don't have to specify the type. In C++ and C# we do however need to specify that we don't want to specify our type. Which is inconvenient.
Now to your example. If we don't want a polymorphic type, then it's perfectly clear to write
pmap := new HashMap();
You're declaring and initializing a variable pmap that's of type HashMap. No ambiguity, less code, readable and writeable. Win-win.
If you want a polymorphic type conforming to an interface, then you'll have to specify the interface.
pmap : Map = new HashMap();
So, what's actually wrong with this? Your argument is just _"I don't know about this..."_ even though it covers all the benefits you've listed. It explicitly lists the interface and the type and is both readable and writeable.
This syntax is just as clear as the other syntax but can save type strokes and reduce redundant noise in certain situations.
@@naxaes7889 To answer the first bit, cleanest to me is:
Vector x{};
Same allocation semantics:
std::unique_ptr x{};
You can get rid of the equal sign completely (Made up Java, C# syntax):
Map pmap{new HashMap()};
@@cemgecgel4284 Yes, that's one alternative. But now you've introduced something worse, inconsistency. It's all about trade-offs and using colon-equal instead of auto/var reduces typing and introduces no downsides.
Your suggestion (the C++ way) reduces typing, but now there are multiple ways to declare a variable. This reduces readability, and searchability, and introduces complexity for analyzers needing to parse the program.
OK, whats the modern way to make a language (tools, etc), because I just scrapped mine
C and Word
23:00 You can define a locally scoped function in C++ by putting a static function in a locally defined class. void DoSomething() { struct Local { static void ReallyDoIt() { } }; Local::ReallyDoIt(); }
I believe anonymous namespaces work as well.
46:28 jonathan blow smaks the shit out of his keyboard
lol
Where is the 3rd one?
th-cam.com/play/PLmV5I2fxaiCKfxMBrNsU1kgKJXD3PkyxO.html
Ok I know I'm late to the party but in the previous video you talk a little about how a new programming language would help prevent the problem where productivity slows due to growing program complexity over time. And here you started talking about how factoring out functions can be bad because it can increase complexity.
But wouldn't Test Driven Development be the answer? That way your "comment" is now in a form a code that actually will run. Then your comment would be tested.
TDD is yet another bad fad paradigm that doesn't solve any real problems, yet creates many new problems. In 10 years it will be gone and the next bad fad paradigm will be here.
One problem with TDD (of many) is that it hugely increases the volume of code you have to write to build anything. As your question puts it ... okay, now your comments are code, but that means you are now responsible for maintaining massively more code than before, and taking the productivity hit of compiling and running that stuff all the time.
It is a trade off for sure.
I've probably been watching too many Uncle Bob videos.
Uncle Bob works in a very different world, where tests can help, and functional programming can help, etc. But that is less about explorative programming which can often be favourable to (indie) game programmers, and rather about customer oriented solutions that fit predefined desired outcomes and have to be easily ported to different databases with only minimal change. Tests can really help with that, but games provide a vastly different environment. Porting to a different games platform means cuddeling with the memory. Tests won't do you any good.
+Kolt Kloud good point.
Does C++ actually specify what the output code of a lambda should be? I would have thought a compiler could either inline it or pull it out into a separate function? (Basically like the "inline" keyword which is basically a suggestion at best)
Yes, the standard specifies that. A lambda translates into a function object with data members corresponding to captured variables.
Max Perepelitsyn
It doesn't specify the output. It specifies what a lambda is equivalent to, in terms of function object classes. It can of course be inlined, and very often is, but so can a function object call.
armpitpuncher Ahh great, that is what I was hoping for.
Inline
func()
{
// some code
{
// more code
}
{
// more code
}
}
And then outline :)
func()
{
// some code
+ ...
+ ... // some comment
}
Although I agree with Jonathan Blow on many things, I think his lack of experience with scheme and lisp shows. He says that they cannot be used for real projects. This is totally untrue. One of the reasons he cites is dynamic typing. You can generate efficient code even with dynamic typing using type inference techniques and basic code analysis. Lisp-based languages have been used for very high-profile software, including video games (such as Jak & Daxter by Naughty Dog using their GOOL language). Scheme has a lot of symmetry that allows some very expressive and productive code writing, including be able to meta-compile that he rates so highly. I also disagree with the syntax making it hard to read. It takes a week to get to used to and you will find syntaxes for other languages overly complex. I will agree with your objections to garbage collection, but even then this field is improving very well.
Also, lambdas in programming languages have been around since the 1950s (not 1960s) when John McCarthy first designed the first lisp, who also pioneered garbage collection.
I think you answered you own question there. As Johnathan stated Garbage Collection is a complete non-starter in many situations that demand performance and/or tight timing guarantees. As is often the case in games, real-time systems and many others. The syntax of Lisp and Scheme are of course very elegant but there is no way normal people are going to accept that for general programming.
@@Heater-v1.0.0 you can definitely do games with garbage collection. You pre-allocate. There's a few games written with lisp. Abuse is one, and naughty dog did a few too.
@@cthutu Indeed you can. You can also pre-allocate in C where it is a more natural, indeed normal, thing to do. Minimising memory allocations/deallocations is necessary for peak performance in all languages.
The question then is: Why would one use one language over another? Why not choose a language that supports what you want to do more easily?
if i declare a function like:
square := (x: float) -> float { return x*x; };
what "square" varyable type will be? a "function" type?
So if i wrote something like:
square : function = (x: float) -> float { return x*x; };
or
square: function;
square = (x: float) -> float { return x*x; };
will it be right?
A function that takes a float, and returns a float, would have the following type:
square: (float) -> float
... I think.
ChocoRX1 As defined in many typeful functional languages (eg Haskell) he isn't being speculative, he is being factual. However, the point is that that IS a function type, it's just a more specific function type than you'd get out of, say, Javascript.
ChocoRX1 No problem! Didn't mean to be accusatory or anything, genuinely trying to be helpful.
C++ is like a movie that didn't care about continuity
Is there any transcript of this talk? Specifically the first 20 minutes or so. I think it would be valuable in text format.
I don't believe there is.
But you're free to make one, if you want to do the work.
39:46
Disclaimer: I'm watching this video as I type. I am not a game programmer. But isn't it obvious that game programming is all about performance? Most of programming suggestions, RAII, law of demeter, dependency injection, etc, are largely guidelines for general software that is rather large. I would think that game programming is a different animal. performance is an issue, and I always thought that most games involved both a physics library and an embedded meta language, often lisp based. is this not true in practice?
Is that why the majority of modern software sucks?
@@etodemerzel2627 what are talking about? Please explain your logic. I truly don't understand. I know the topic is about game programming, but then you said the majority of software sucks. I would hope that you will agree that majority of software is not games.
@@larrytroxler7017 There's a somewhat unpopular opinion that modern software is really slow and bulky. And it seems to me you were implying that non-games don't care about performance?