12 ms·
A Reply to “Let’s stop copying C”
- discreteevent 7y agoThey are right about null. To say it is a billion dollar mistake is completely over blown. Maybe it was at the time but now a null pointer exception is extremely easy to find and fix. It doesn't compare to the other complex bugs one has to deal with most of the time.
- iainmerrick 7y agoIf when you say “null pointer exception” you’re thinking of Java: do you find any value in nullable / non-null annotations? Or if you’ve used Kotlin, its non-nullable references? I find them very useful. I don’t just want null pointer exceptions to be easy to debug, I want to avoid them completely in the first place.
- tlbsofware 7y agoA really effective way to handle nullable fields is an Optional<T> which forces the user to handle a null value for that field, this is very useful when working in a team and dealing with objects that someone else spent time creating, it won’t get rid of null but it definitely hints the user at “hey this field may be null so I should go ahead and handle this before I even get to testing my code” Edit: You would want to implement these in your getters for fields that may return null https://docs.oracle.com/javase/8/docs/api/java/util/Optional.html https://docs.oracle.com/javase/8/docs/api/java/util/Optional...
- einpoklum 7y agoNote that in C++ today, static analysis tools + classes such as `std::unique_ptr`,`gsl::owner` and `gsl::non_null` (similar to the Kotlin mechanism you described), it is possible to avoid the possibility of null pointer dereferencing. That's not to say the same thing is possible in C, but one _can_ go a long way with static analysis at least, and suspicion vs provability regarding pointer dereferences.
- chongli 7y agoYou're right. The mistake is not in having null pointers, it's lacking non-nullable references which is the problem. If you can write a function which only accepts a non-null reference, then you don't have to check in every function called from it.
- AgentME 7y ago+1. I think this is something that gets lost in discussions around null. Generally, everyone complaining about C's handling of nulls will recommend languages that make nullable-pointers opt-in by having non-nullable references and making them the default. The argument people make is not that C should somehow drop the entire concept of null; the argument is that people should use languages where a type has to opt-in to accepting null.
- dehrmann 7y ago> do you find any value in nullable / non-null annotations Hah! Annotations. They're tales told by idiots, full of sound and fury, signifying nothing. But seriously, they're about as meaningful as reading the javadoc. At least Optional enforces the contract; it's just clunky.
- smoyer 7y ago@NonNull etc are not completely meaningless ... if you run SpotBugs it can be set up to fail your build if the contract specified in the annotations isn't met. I love static analysis!
- iainmerrick 7y agoYou can configure your IDE to warn of incorrect usage, and you can configure your compiler to flag warnings as errors. That will catch a lot of potential problems at compile time. It’s not perfect but it can be lot better than nothing. Java annotations are better than Optional when you’re interoperating with Kotlin code, such as when you’re partway through converting a large codebase to Kotlin.
- claudiawerner 7y agoI'm curious; I had an idea (in fact, it's part of my Master's degree project) to create a language that brings over concepts from functional programming to systems programming. What would be beneficial for that, and what wouldn't be?
- convolvatron 7y agoclarity: this is arguably not FP specific, but I think that having declarations for binary layouts would save a lot of very confusing shifting and masking concurrency: to the extent that you can get by with pure functions, they also tend to be more trivially multithreaded. you also have quite a bit more flexibility wrt things like STM, or lazy evaluation, or other more novel notions f concurrence scheduling regions: I think GC is pretty much a no-no, at least for some critical sections. but there is a lot you can do with static analysis to both make allocation more statically safe and more performant than generic heaps closures: again, not specific to FP..but using closures makes asynchronous programming really quite nice runtime: having a proper runtime with maps, higher order functions and and real string functions when not in the performance path really does save a lot of time. immutability: not sure its a win, but I find it super instructive to think about what actually has* to be mutable in a big system..C really loses the distinction since with the exception of the verb and viral const, everything is mutable by default. you can also* play lovely tricks with explicit time ala MVCC and Daedelus even the sum of that I think doesn't really justify a project...but if you're doing it anyways...why not try to make something a little nicer than the huge and difficult to debug standard C business
- Gibbon1 7y agoI don't have much to add but I do think you are correct about binary layouts. Correct about closures. I think with closures you can fix a lot of C's jankiness. Immutability. I've never been happy with 'const' as a 'bandaid' for mutability issues.
- abjKT26nO8 7y agoC will not throw an exception when you try to dereference a null pointer. C does not have exceptions. Instead, dereferencing a null pointer results in undefined behaviour, which is often tough to deal with. Having an exception thrown is a lot nicer. In C the compiler is free to make optimizations, based on the programmer's vigilance about which pointers may or may not be null, which you would have never expected on your own[1]. Debugging these issues is hell. [1]: <https://www.imperialviolet.org/2016/06/26/nonnull.html> https://www.imperialviolet.org/2016/06/26/nonnull.html>
- MaxBarraclough 7y agoI think their point was that in modern languages (not C), having null in the language isn't so bad, as attempts to dereference null are handled fairly safely, generally with exceptions. Same thing goes with modern languages throwing on signed overflow, where, again, C gives you the horrors of undefined behaviour.
- ChrisSD 7y agoI think the main issue is that the program should never reach a state where you're in a position to dereference a null. If it does, that means you didn't handle an error condition earlier in the program. Sure you can handle the null safely but that doesn't handle the real cause of the error.
- MaxBarraclough 7y agoI agree that's also a valid point. The solution is for the language to offer 'option types', which essentially force you to check for null. Zig does this, and the result is much nicer than how things work in C: https://ziglang.org/documentation/master/#Optionals https://ziglang.org/documentation/master/#Optionals
- ctidd 7y agoAnd options help with solidifying input contract as well as the output. Knowing what's an acceptable input via a type system is just as relevant as knowing what is a potential output.
- coldtea 7y ago>They are right about null. To say it is a billion dollar mistake is completely over blown. Maybe it was at the time but now a null pointer exception is extremely easy to find and fix. The whole idea is not to have to "find it and fix it" at runtime... Whether it's "easy" or not it's a moot point, as it is after your program just crashed or got into an undefined state, while your server is running or your desktop user is using it...
- hinkley 7y ago"Easy" is still an opportunity cost versus "once". I wish my coworkers all understood that (are you hiring?)
- dathinab 7y agoYup, plus depending on the case it might be anything up to insanely hard to find that bug (imagine >100k line code base where will causes a subtile wrong state once in a thousand requests if build with optimizations under load which causes a chain of other subtile errors crashing the production server once a hour).
- agentgt 7y agoI have to agree (ignoring the whole C doesn’t throw exceptions). I have been looking at our bugs, logs and exceptions recently of the past 6 years and an enormous amount of bugs are caused by methods/functions that have multiple parameters with the same type (Java). This happens because (my theory) we use java and java doesn’t have type aliases or value types as well as easy destructuring. It also doesn’t have named parameters (well there is a compiler option to retain the parameter name but it’s not like ocaml label parameter or python kwargs). So often times in boring business programming you are dealing with methods with 5 to six strings so it’s very easy to mix up the parameter order. Very few “hard” bugs were caused by NPEs where as the previous problem caused serious pain.
- hinkley 7y agoThe worst transposition bug I ever found had survived 14 months from inception. I was... appalled. Stringly typed code is a scourge.
- AndyKelley 7y agoIt's less about null specifically and more about taking advantage of a type system. A null pointer, or, really, any pointer address used as a special value, is an in-bound value. This makes the type system unaware of it. By using optionals, the idea of a special "missing" state becomes an out-of-bound value, meaning the type system can participate in helping the programmer properly deal with missing values when the time comes. So it doesn't even have to be null. Let's say you're writing a function that returns an integer, or MAX_INT indicating that something failed. That's the same problem. The error indicator is an in-bound value, which means the type system is unaware that MAX_INT is special, and won't be able to catch mistakes, such as using the return value without checking if it is the special value. It would also be a subtle bug if the integer size changed from int to long, and now MAX_INT isn't even the correct value anymore. If the type were an optional, everything would have continued working correctly.
- gingerBill 7y agoSentinel values are things the compiler has no idea about and I agree with you here. If the type system has no idea about these sentinel values, the compiler cannot help you with those edge cases. One aspect I think many people are bringing up, implicitly, is that they want a language that enforces a form of "correctness" so that you cannot put your programming into an "invalid state". I understand the appeal of this but it is not free, it does come at a cost. This implicit view assumes that all values are a form of "object" and "singular". Assuming pointers are "references" to a "singular object", rather than just a pigeonhole to a piece of memory which may have a type associated with it. This may seem like it is expressing he same concept, but the former is kind of like Aristotelian Object Orientated Ontology (OOO) naïvely applied to things which are not actually objects in the real world. Thinking of these things as "objects" is an abstraction and is not actually what is happening. It may have a lot of utility, but it is not the reality at hand. In many regards, most forms of Object Orientated Programming (OOP) in reality are an application of OOO (even the term "virtual" is from Aristotle). And Rust's ownership and lifetime semantics are another application too. But explaining this is in itself a long article. I know I won't convince people here about this, and that's absolutely fine as I don't expect to. It's just interesting to read many people's implicit assumptions about what how things "ought to be" with regards to a programming language.
- mjevans 7y agoNULL / nil isn't bad; however any case where encountering one is a problem is most often an issue of under-specified program design. The better question is, what were you expecting there and how can it be described without a bare pointer? I often find a list is better, particularly in languages with syntax sugar for iterating through a list.
- nickmqb 7y agoAgreed. Non-null pointers also come with their own set of problems: - Increased language complexity. Initialization of (arrays of) structs with non-null pointer members is more complicated because there is no straightforward "default" value that can be used. - Performance tradeoffs. Initialization of arrays and other containers is more costly for non-null pointers (or structs containing them) because we cannot simply zero out a block of memory. - In memory unsafe languages it is possible for a non-null pointer to become null if its memory is overwritten (e.g. in custom allocator scenarios (also mentioned by the author of the article), interop scenarios, etc.). The type system now makes a false guarantee, and it becomes possible for a "non-null" pointer to trigger a null pointer crash. (It's worth mentioning that enums generally have a similar problem). - I'd like do a more rigorous evaluation on this, but my gut feeling is that most null pointer bugs that I've encountered usually happen in situations where the pointer would have to be declared as nullable anyway, because I'm using null to represent a possible state.
- TheDong 7y agoI don't believe any of those tradeoffs are true for rust, which has references that may never be null (in safe rust). I'll respond to each in turn: - language complexity for arrays of nullable things In rust you can easily type 'let v: Vec<Option<&MyStruct>> = vec![None; 10]' Having a vector of 10 'None' option types is no more difficult than anything else. Also, Option<T> implements the default trait. It defaults to 'None. - Performance Rust's 'Option<Box<T>>' takes up just as much space as 'Box<T>'. The null value for the pointer is used as the None type for the option. In that case it's a zero-sized zero-overhead type. You can read about that here, and in various other places: https://doc.rust-lang.org/std/option/#options-and-pointers-nullable-pointers https://doc.rust-lang.org/std/option/#options-and-pointers-n... - In memory unsafe languages it is possible for a non-null pointer to become null Yeah. In memory unsafe languages. Don't use one of those. In Rust, Haskell, etc the Option type can't lie like that without explicitly opting in to memory unsafety. Which hardly anyone does. - my gut feeling is that most null pointer bugs that I've encountered usually happen in situations where the pointer would have to be declared as nullable anyway Null pointer exceptions happen when a pointer is nullable, but some location forgets that it is. If the type-system encodes this in the form of Option<T>, it's impossible to forget that. If I have an 'Option<String>' and I have a function that expects a 'String' (but not a null string), with the option type I know I have to do something like 'call_func(val.unwrap_or(""))', or I have to match, or I have to do 'let Some(val) = optional_val { call_func(val) }' before I can use that thing. The fact that I have to do a null-check is built into the language and the compiler yells at me if I forget about it. Being able to encode in the type-system that null should encode some valid state (e.g. 'None' in the option type) vs null-able pointers, where you use pointers that may be null even when you don't want nulls and the compiler can't check you work.. It's night and day difference.
- unlinked_dll 7y agoI believe that's a reference to Tony Hoare's famous talk about the invention of null references which he calls his billion dollar mistake https://youtu.be/ybrQvs4x0Ps https://youtu.be/ybrQvs4x0Ps
- WalterBright 7y agoI wrote an article about this, "C's Biggest Mistake": https://www.digitalmars.com/articles/b44.html https://www.digitalmars.com/articles/b44.html
- Animats 7y agoAgreed. I've been saying that for too many decades. My version of that is "The big safety questions are 'how big is it', 'who owns it', and 'who locks it'. C helps with none of those." Most of the things done with pointer arithmetic are really array slices without the right syntax. If you're doing something with pointer arithmetic that can't be represented an array slice, you're probably doing something wrong. I once proposed adding slice syntax and array sizes to C. This was discussed on comp.lang.c at some length, and looks backwards compatible. But the political problems are huge.
- WalterBright 7y ago> But the political problems are huge. That's true. But there are no technical problems with it, not even backwards compatibility problems.
- Gibbon1 7y agoI feel like the resistance is people think pointers are C's secret sauce. Reality is the only advantage is it makes it easy to write bran damaged unopimized compilers for C. Which no one does. I remember back in the 90's my boss gave me shit for using array indexes instead of pointers. Being a brat I looked at the assembly and there was no difference. Ditto the cultural proscription on passing/returning small stucts by value. In practice it's no worse than passing the arguments separately. And likely easier for the compiler to optimize around.
- Animats 7y agoDitto the cultural proscription on passing/returning small stucts by value. Which really should be a compiler decision. Depends on the target CPU. If you pass something as const ref, the compiler should copy it if that's faster. For AMD64, it probably is. If something was just written, it's in the L1 cache and is really cheap to copy. On-chip data buses today can be as wide as 64 bytes. Anything not bigger than that is better copied.
- dang 7y agoThe referenced article was discussed last year: https://news.ycombinator.com/item?id=18977460 https://news.ycombinator.com/item?id=18977460 and a bit at the time: https://news.ycombinator.com/item?id=13079341 https://news.ycombinator.com/item?id=13079341
- axaxs 7y agoI'm having trouble following the negative modulo wording or formula. It says % and %% are identical for unsigned integers then below defines it as a %% b == ((a % b) + a) % b If that's the case I can't figure out how they are identical for unsigned integers. Or is that example only a hint for how it would work with negatives?
- klyrs 7y agoI don't precisely follow the text either; it looks like that formula computes ±(2a)%b. Perhaps it's a typo for a %% b == ((a % b) + b) % b Which brings a negative modulus into the range [0, b)
- mjevans 7y agoI think you are probably correct. Lets try a set of simple cases. ( 4 % 3) == 1 ( 4 %% 3) == 1 Are these two correct answers? (-4 % 3) == -2 ?? (-4 %% 3) == 1 ?? Lets assume I got that correct and plug in some numbers. a %% b == (( a % b) + **b** ) % b -4 %% 3 == ((-4 % 3) + 3 ) % 3 -4 %% 3 == (-2 + 3) % 3 -4 %% 3 == 1 However I think it would be clearer for maintenance if the extra operator wasn't used and instead some 'math.absolute()' function were used. PS: Hopefully those are short enough to not die on mobile
- klyrs 7y ago4%3 is 1, so -4%3=-1=3-1=2 (mod 3). I disagree about maintenance: use the right formula and do careful unit testing. Abs isn't a great help here either.
- Vassvik 7y agoIt's definitely a typo. Conceptually I prefer looking at them as a % b = a - trunc(a / b) * b a %% b = a - floor(a / b) * b The latter is even used as % in languages such as Python, which further compounds any syntactical confusion. :D
- gingerBill 7y ago
- einpoklum 7y agoMost of what the author suggests is available in (modern) C++ - while maintaining backwards compatibility with C code: * Textual inclusion: C++20 has modules, where you include sematically, not textually. * Bitwise operator precedence: Not soundly supported, but you can sort-of have custom infix operators in C++, see: https://stackoverflow.com/questions/15632217/defining-new-infix-operators https://stackoverflow.com/questions/15632217/defining-new-in... so, specifically, one could implement the alternative modulo. * Leading zero for octal: C++ has custom literals, so you could define a _octal or _hex etc and get the value determined at compile-time. * No power operator: Same thing in Odin and C and C++, IIANM. * Switch with default fallthrough: Since C++17, there's an official [[fallthrough]] anottation. You can make your compiler fail in cases when you implicitly fallthrough. So, not as elegant, but you can ensure you don't mess up and get the wrong behavior. * (Type syntax: Nope, C++ has the contrived syntax of C.) * Weak typing: With a library, you can have strong aliases which are not interchangeable with the aliased type. See this blog post: https://foonathan.net/2016/10/strong-typedefs/ https://foonathan.net/2016/10/strong-typedefs/ and the library it links to. Things would be better, though, if the C++ standard committee allowed for an operator. * Bytestrings: This is a library issue really. And both C and C++ have libraries which deal with wider characters, with UTF-8 and what-not. * ++ and -- : You don't _have_ to use them... and it's possible to have static tools which forbid them in source files. * ! Operator: C++ has !, && and || , but also "not", "and" and "or". I like the latter. * Multiple returns: It's easy to return a tuple in C++, and with C++17 you can even construct and initialize multiple variables like that, e.g. `auto [index, name] = get_index_and_name(whatever)`. * Errors: C++ is multi-paradigmatic here, supporting the traditional status return, an expected<T> type (either an actual T or an error; not yet standardized by available via widely-used ibrary), and exceptions. Monadic-style programming is not yet supported and will likely not be in C++20, but there are discussions about this. * Nulls: With `std::optional` and `gsl::non_null`, and especially with no return types necessary, you can comfortably avoid using `nullptr` in C++ code. So - with some choices and a little discipline (really not much!), you can have all of this "Odinish" behavior you like. Of course - the price of C backwards compatibility and supporting multiple programming paradigms is complexity of the formal language definition, grammatic ambiguity, and a syntax that is not always pleasant.
- 7y ago
- zzo38computer 7y agoI do not agree with all of them. I think assignment expressions is good, and textual inclusion is good (but should not be the only kind of inclusion), and increment/decrement operators is good, and macros is good (although the way C does it is not good enough; there are many things it doesn't do), and pointer arithmetic is good, and goto is good. But I agree with them that the C syntax for octal numbers is bad, and the C syntax for types is bad. Identifiers should not have hyphens if the same sign is also used as an operator. Strings should be byte strings which have no "default" encoding other than ASCII (although UTF-8 and other encodings are still usable too, if they are compatible with ASCII). Another problem with C is that it does not allow null declarations, and does not allow duplicate declarations even if they are the same. There are many other problems with C as well; some kind of low-level stuff is too difficult with C.
- RealityVoid 7y ago>> some kind of low-level stuff is too difficult with C. Surprised to hear this. I do C for a living, including some low level stuff (not x86) and there aren't a lot of low level things I feel I couldn't do. Can you expand a bit?
- jstimpfle 7y ago> Another problem with C is that it does not allow null declarations, and does not allow duplicate declarations even if they are the same. What do you mean here?
- zzo38computer 7y agoWell, it works now (so that problem no longer exists); but on the computer I used before this one, it didn't work.
- eximius 7y agoHadn't read the original essay, but Eevee is always a great blog. Honestly, Rust hits a lot of good points for me. My only concern so far has been `.await` and a minor concern that they'll keep adding junk and end up like Perl with too many features.
- DarthGhandi 7y agoIt's sugar for existing features. "await"ing existed before but required some really messy syntax to achieve the same thing. It's very much an incredible improvement over the status quo.
- eximius 7y agoI'm aware. I just believe that the design decision around `future.await` was dumb. Preface: this is a minor syntactical annoyance that aesthetically and in principle annoys me a great deal, but in practice is unlikely to cause much confusion. The rest of this should be considered a light hearted rant. `future.await` looks like it should be field access, not running arbitrary code for a future. It should have either been a keyword (a la `await future`), or some language built in trait method (like `future.await()`), of which there is precedent for things that require compiler magic. The arguments against macro or method like syntax were "but it's not a function in the mathematical sense", which is ironic given they chose something that looks like field access, and irrelevant in that I could have a function call inline assembly and just jump elsewhere, ignoring their concept of mathematical functions and stacks anyway. I'll admit that the post-`future` syntaxes have the benefit of chaining, which is why I prefer `future.await()` over `await future` or `await!(future)`, but I still stand by that `future.await` was the wrong choice. So my main problem is that they made a poor design choice from what seemed an obvious pool of candidates (to me). The worry is that it'll happen again for more bizarre features that happen to be lobbied for. /Endrant
- tele_ski 7y agoI'm also in this camp, I did read through at the time some of the arguments for this syntax and it allows for some elegant chaining of awaits and other things I can't recall, but I still can't get over the fact that it looks like a member access and also for such an important meaning to the current line of code it isn't boldface right at the start screaming "im async". Nope it's at the end of the line and doesn't standout in any meaningful way since it's a member access. I find it very unfortunate
- AgentME 7y ago>NULL/nil is just one of many invalid memory addresses, and in practice most of invalid memory address are not NULL. I want a language that can check at compile/check time that none of my pointers will have invalid addresses. Recognizing null as just another invalid value makes it more obvious to me that I want a language to handle it differently than how C does it. In memory-safe languages, it's already unthinkable (/ very rare outside of situations where you're deliberately doing unsafe/native code integrations) to get non-null invalid pointers that point to a different type of thing than you want. When was the last time you had a Java program crash because a typed reference actually had a different type of reference at runtime somehow? Isn't that great how that basically never happens? -- But if you consider nulls a different type of reference, then it does actually happen sometimes. It would be great if we could try to close off that issue. I'm a huge fan of languages with non-nullable references as the standard, like Kotlin, Typescript, and Rust. In my experience, it's much easier as a programmer to understand how a codebase uses nulls when the codebase is in a language with default non-nullable references, and therefore null-related issues where a future programmer passes a nullable value somewhere it shouldn't be happen much less often.
- enriquto 7y ago> I want a language that can check at compile/check time that none of my pointers will have invalid addresses. This is incompatible with pointer arithmetic, as it would require to solve the halting problem. You could, still, verify it at run time.
- AgentME 7y agoRight, but another option is to eschew pointer arithmetic. Iterators in many languages address many usages of pointer arithmetic, and can be designed to compile to the same sort of code as pointer arithmetic-using C compiles to. C wouldn't ever be able to take out pointer arithmetic, so I worry anyone envisioning a new language as some diff from C is probably going to get stuck on that sort of thing too. I'm a big fan of the original referenced article by Eevee for bringing this sort of thing up.
- 7y ago
- jlebar 7y agoTIL the C variable declaration syntax is meant to evoke how you would use the variable. For instance, int *x[3] means that to get an int, I type *x[3] (well ok, 3 would be a poor choice). Whereas if I have int (*x)[3] I'd dereference it as (*x)[0] meaning, it's a pointer to array of ints, while the first is an array of int pointers. This is mildly life-changing.
- ensiferum 7y agoYou got an off by one bug in your first example ;-)
- jlebar 7y agoYou mean "(well ok, 3 would be a poor choice)"?
- wruza 7y agoIf we used languages with 1-based arrays, then the first element was 1, the last was n, and find(x) could return 0 as an “item not found” marker instead of returning -1 or worse uint_max. Reasoning about counting from the end could be off-by-one easier: n+1-i instead of n-i-1, n-1-i or n-(i+1), whichever nonsense you like better. Looping: i=1, i<=n. Setting next: a[n+1]=x, where the capacity allows. But when you mention one of these languages, you get a bunch of “oh, 1-based arrays, so uncool, not an option”.
- mckinney 7y agoI hadn't given Odin a look before I read this post. As a fellow general purpose language author (Gosu) I applaud the author's pragmatic, albeit unpopular at times, point of view. For instance, his position regarding Null is spot on re the "streetlight effect" reference. Others include: * pascal-style declarations * type system * multiple returns * strings * switch Also I would not downplay the advantages of operator tokens && ||. More than just familiarity with C-family developers, in my experience, they are more generally effective as expression delimiters. They stand out better than 'and' 'or', which is better for readability.
- kazinator 7y agoThe direction of modulo with negative operands (and of division) was implementation-defined in C90. At least it is pinned down now. Common Lisp has a number different modulo operators that go every which way. Firstly there are the single-valued mod and rem: http://clhs.lisp.se/Body/f_mod_r.htm http://clhs.lisp.se/Body/f_mod_r.htm Secondly, the floor, ceiling, truncate and round functions also return a remainder: http://clhs.lisp.se/Body/f_floorc.htm http://clhs.lisp.se/Body/f_floorc.htm
- kazinator 7y ago> foo bar; // Is this an expression or a declaration? You need to check the symbol table to find out* Checking the symbol table is a simple one-liner. Pushing all semantic information into the syntax so that the meaning of everything is known without any lookup is not realistic and will result in a bad language. Lisp: (a b) ;; what is this? You need the full surrounding context to know whether it's b applied to a, value of b being bound to variable a, or a list of base classes a and b in a defclass or what, ... and it's good that way.