7 ms·
Crafting Self-Evident Code with D
- smitty1e 3y ago> I mean literally in the literal sense of the word. A literary triumph.
- extasia 3y agoLiterally^
- ilitirit 3y ago> Try not to create functions that both ask a question and modify state. Over time, I’ve been gradually splitting such functions into two. AKA Command-query Separation (CQS) https://en.wikipedia.org/wiki/Command%E2%80%93query_separation https://en.wikipedia.org/wiki/Command%E2%80%93query_separati... I use this often, but there is one typical scenario where I break this rule - creation commands. Often someone calling a command that creates an entity needs the ID of whatever was created. Yes, you can use UUIDs and pass this to the creation function (or use more complex methods), but I haven't really come across scenarios where this adds anything meaningful, not to mention that it's not always possible. So I generally break CQS for creation commands, i.e. they change state and return a value. EDIT: I just noticed the Wikipedia article cites other examples, e.g. The `pop()` method on a Stack.
- Joker_vD 3y agoCQS is one of those things that make sense on the first glance, but the more you look at them, the less sense they do. I mean, if you send a (synchronous) command, shouldn't you at least get a success/error indication as a return value? I've met someone who argued against even that, insisting that all commands must be void nothrow, and I should use separate queries to figure out the results. And yes, I should generate UUIDs at every interaction and hope that collisions don't arise, and if somebody tries to do a replay attack, well, that's such an unlikely scenario it can be safely disregarded. There is a reason that procedure/function split has been largely abandoned in the 80s; but at least procedure could have var/out parameters or have pointers for arguments.
- cjonas 3y ago> I should generate UUIDs at every interaction and hope that collisions don't arise Assuming you're using uuidv4, the chances of a single collision is so low you don't need "hope"
- KMag 3y agoAt my last job, I occasionally ran into people worrying about SHA-256 collisions for non-maliciously created build artifacts, so I wrote an internal wiki entry with the back-of-the-envelope estimates. If you're really paranoid, use getentropy() to seed AES in counter mode, and generate 256-bit cryptographically pseudorandom IDs. Assume your system consumes 1 trillion (2^40) IDs per second for 1 trillion seconds (34,000+ years). The probability of a collision over that time frame is roughly (2^(-(256-80)/2)) = 2^-88. (Actually, in counter mode without throwing out any bits, the probability of collision is even slightly lower than this random oracle model suggests. This is true even if there are multiple independently seeded streams, as long as they're all seeded with high-entropy sources.) Assume the probability of a life-threatening dinosaur being cloned in your lifetime is one in a billion (2^-30), and if so its escaping is one in one million (2^-20), and if it escapes the chances of it entering your house and you being able to save your life by looking for it is one in one billion (2^-30). In this case, it's roughly 256 times more rational (assuming your death and the consequences of an ID collision are equally bad) to check under your bed for dinosaurs vs. checking for ID collisions. Also, the probability of radioactive decay flipping the comparison result bit at the exact moment you compare you random 256-bit IDs is much much higher than the probability of collision. So, if you're paranoid enough to check for collisions, you should be checking multiple times. Of course, the above analysis all hinges upon correct implementation and high-entropy seeds. These are the real weak points of using large random IDs, so audit and test your code early and often. Carry out the above analysis with 122 bits of entropy for UUIDv4, substituting your actual system lifetime and expected consumption rate, and you'll likely find similar results.
- danielEM 3y agoMy first rule is - if you don't need it (SHA, UUID, etc...) - don't use it. My second rule is - don't be a priest - if someone did it and it works then it works. Assumption that close to impossible collisions don't happen is a belief, not a proven mathematically fact. ;-) Such a problems are also more complex than just 1 dimensional collision math.
- gdprrrr 3y agoIs that the opposite of tell-dont-ask?
- TheFattestNinja 3y agoI find myself more and more over the year falling back to creating lots of "getOrCreateX(args)" and I must say I still haven't found a single scenario where this is worse than "get" and "create": 1) It helps with encapsculation of race conditions (you don't need to acquire locks outside to do if(get==null) { create } when you can have it in the function itself) 2) I normally don't care in my code if this is a pre-existing instance or one that I just created. I just want it now to use. I personally find this approach superior to CQS.
- dgb23 3y agoThe separation often makes sense with long lived data. A lot of data is fleeting. For example when you construct a struct out of sub-pieces. Many of those probably just live on the stack or are otherwise temporary. This type of data is passed around, read, written to etc. until it has the right shape. It goes through a little pipeline and then often becomes part of a bigger, longer lived structure or at least has some impact on one. Long lived data is different. It's often global or is at least seen by entire modules and so on. There it makes sense to think of commands and queries. A database is a typical example. The core idea is to make mutations obvious. This pattern emerges in different programming domains and has an influence on a lot of programming decisions: UIs, DBs, video games, paradigms, cloud architecture, managed caching, HTTP (safe vs unsafe methods) etc. under different names, so there's an universality to it. But... I think if we learned anything from the paradigm craze(s) is that any pattern is useful until it isn't.
- hinkley 3y agoLong lived data tends to have additional value to it, and anything valuable gets loaded up with business rules. Data that sticks around and hardly ever changes, we start layering more interpretations on top. Eventually for performance reasons, those functions turn into projections of the data that get pre-cooked instead of interpreted on every request. Materialized view. Data transformations into other tables, etc.
- chowells 3y agoYou should generate UUIDs on create requests not for some ideological purity, but for the API to be idempotent. When the network gets unreliable, idempotence + retries gives you effective exactly-once delivery.
- hinkley 3y agoAll I recalled was Meyer talking about this, I couldn't remember what it was called. Another technique that is in the same category is functional core, imperative shell, which does largely the same steps but has a few more opinions about how you organize said command and query separation. Every time I've switched code to this structure it's shrunk my tests by at least half, sometimes much more. It feels so good when you stop.
- addicted 3y agoExcellent stuff. I have to disagree with the not using the source code formatters bit though. More trivially, you can improve the source code formatters to accept the example provided as legitimate. In fact, it can be setup to automatically format other similar patterns that way itself. Yes humans are good at patterns but there’s only so many patterns you can form while writing code, and the heuristics involved while deciding what pattern to use can almost certainly be encoded fairly easily. But even if we assume that this isn’t possible, a formatted is still very valuable for the simple reason that others may not follow the same patterns as you, or may not follow patterns. Someone else formatting the code differently may rub you the wrong way, and the only way to resolve that is the worst type of code formatting bike-shedding. And if you don’t agree and both of you do your own thing in different parts of the codebase, well, you’re not following a pattern and you’ve lost the benefits indicated about following patterns.
- mhh__ 3y agoFormatters can also readily spot and preserve patterns if you want them. You can shape the cost function to value patterns. I suspect Walter formed this opinion when they were much worse. Also formatters have enormous benefit when you have lots of people in the same project. It almost doesn't even matter what the format is as long as it's efficient IMO.
- Lutger 3y agoExactly. Walter Bright also used to be a lone hero - he isn't anymore. But some habits stick of course. Also the languages he writes in might not have the best formatters. I am old enough to have experienced the not so good formatters (in mainstream languages) and wasn't a fan. But after working with really great formatters, its such a benefit I don't like to go without it anymore.
- whstl 3y agoI can see his point. I'm also perfectly fine with programming without a linter/formatter for personal projects or bigger projects where quality is more important than speed. The code doesn't look worse. In fact it might end up better. But for corporate teams, doing web dev? It is definitely necessary, unless you want to spend days reviewing trivialities, or you're ok with the code looking like a patchwork quilt of styles.
- FpUser 3y ago>"#define OlO putchar" >"#define O10 exit" Crime against humanity?
- omoikane 3y agoThis is the kind of thing that would end up in IOCCC. In fact, it came from here: https://www.ioccc.org/years.html#1986_bright https://www.ioccc.org/years.html#1986_bright
- michaelcampbell 3y ago#define BEGIN { #define END } > Believe it or not, this was common C practice back in the 1980s. Was it, though? I used C back then (granted, learning it, and remember reading posts from Walter on what I think was FidoNet? BBS based, regardless), and never once saw anything like this OTHER than the stories of the bourne shell source code. But, "common"? Feels kind of strawman.
- defrost 3y agoHuge strawman and not at all common outside of the "we invert the norms up to 11" crowd. The article opens with excerpts from the authors entry into The International Obfuscated C Code Contest which indicates the kind of crowd that treated such hijinks as commonplace. You didn't see such things in dull boring let's just make simple code that works projects. You'd see all that and more in the end of town that wanted to sketch out new language ideas ... the first "working" C++ implementations were made by swapping out the default C preprocessor for a more powerful text manipulation engine and really going to town on the macro magic. It looked like early C++ but it went through a text mangling and came out as C and went into C compilers of the day. ADDENDUM: I'm pretty sure the first C++ mockup I saw | worked with was the FrankenChild of source.txt | M4 preprocessor | C compiler | Asm | link https://en.wikipedia.org/wiki/M4_(computer_language) https://en.wikipedia.org/wiki/M4_(computer_language) but that's going back a ways.
- michaelcampbell 3y ago<nod> thanks, yeah I was actually a student at UCF where some OCCC winners were instructors (David Van Brackle and Mark Schnitzius, if memory serves). And, I actually used `cfront` in my first job, and used the parameter to see the C it spit out as a way to learn how C++ did some things. Pretty interesting, if, as you say, nearly unreadable to humans!
- zabzonk 3y agoi'd just like to point out that cfront was not a preprocessor - it was what today would be called a transpiler - it compiled c++ code into c.
- bluGill 3y agoI have to disagree with the enum advice. "enum { No, Yes };" is bad (regardless of order) but the solution isn't use bools. The solution is to use a better name "enum { DoNotFoobar, WithFoobar };" is what you should be using. (C++ has scoped enums so you might see Foobar::with or Foobar::without - possibly better if you have that type of option in your language). If you do it this way your code is even more readable, and you can someday realize there is another option as well. The point of this is DOWN WITH BOOL!
- queuebert 3y agoGeorge Boole weeps.
- frou_dh 3y agoAlso, for example if you are implementing chess then you should have an enum of Black and White rather than using `bool white`.
- WalterBright 3y agoBlack and White are two states where you should never use negation, and hence bool is not the right choice for self-evident code. I.e., you should never see this in code: if (piece != Black)
- codr7 3y agoWhy? If there are only two states, and provably will never be more; it's either or.
- WalterBright 3y agoBecause negation is a cognitive problem people have. The other problem happens when a third state is added - it becomes very hard to find all the dependencies that need updating. A classic case of this: if (!Windows) ... do something for Posix ...
- RcouF1uZ4gsC 3y agoD is a beautiful language. However, its embrace in the 1990's of garbage collection has proved its Achilles heel. Before anyone says you can write D without the garbage collection, yes you can but then you lose access to most of the standard library, and likely most third party libraries. Rust and C++ allow you not to have to use garbage collection but still use the standard library and 3rd party libraries. With garbage collection, D is not directly competing not with C++ but with Java, Swift, Go, C# which have far more support across the industry.
- Alifatisk 3y agoIt is indeed a beautiful language, what makes the GC such a no-no? Hard to work with in embedded systems?
- mhh__ 3y agoOpposition to GC is mostly people being fooled by GC-ed languages tending to be higher level and thus generating more garbage. > Embedded systems In the embedded work I've done I barely allocate at all, so it matters not GC or otherwise.
- daaaaaaan 3y agoSwift doesn't have garbage collection.
- RcouF1uZ4gsC 3y agoRefcounting is a form of garbage collection. While C++ and Rust also have refcounting, it is the default memory management mode in Swift in a way that it is not in C++ and Rust (where the defaults are more using stack based or unique_ptr/Box before reaching for refcounting).
- daaaaaaan 3y agoNo, it's not. Refcounting CAN be a garbage collection algorithm, but in Swift it's deterministic and done at compile time. Not to mention recently added support for non-copyable types that enforces unique ownership: https://github.com/apple/swift-evolution/blob/main/proposals/0390-noncopyable-structs-and-enums.md https://github.com/apple/swift-evolution/blob/main/proposals...
- Izkata 3y ago> What’s the clue? It’s got a little tire for a knob! Pull the lever up, and the gear gets sucked up. Push it down, the gear goes down. Is that self-evident or what? Well, I didn't figure it out. Definitely easy to identify if you've heard this before, but that's not something that's self-evident. > enum { Yes, No } is just an automatic no hire decision This comes from process management where 0 is success and anything else is an error code. Shell conditionals are this way around because of that. Depending on the context it could be an indicator of someone who really understands the system but isn't yet familiar with more general coding idioms. > The More Control Paths, the Less Understandable Woah no, I feel like I agree with the title but that is a terrible example. It looks like Z happens even in Not Z situations, which requires digging into everything to find out what's actually going on. The code is lying to you, which is a really bad setup. This could be mitigated by just renaming doZ() into something that hints it won't always occur, maybe tryDoZ() or checkDoZ() or something.
- WalterBright 3y agoConsider another example. There was a case where the pilots had set the automatic air pressure regulator to "manual". As they climbed past 10,000 feet, a warning horn sounded because the cabin air pressure was too low. The horn sounded similar to another horn, and the pilots worked the other horn problem. Then, they lost consciousness from lack of oxygen and crashed, killing all aboard. This is not a self-evident user interface. A better one would be to change the beep of the warning horn to a voice: "alert: air pressure too low". Now the pilots save precious seconds figuring out what's wrong and how to fix it. The horning could be even better: "alert: air pressure too low. Don oxygen mask". With the landing gear lever, maybe a pilot in training needs to be told what it does once. But he'd never have to be told again.
- WalterBright 3y agoAuthor here. AMA
- trollerator23 3y agoWhat are you up to these days, Just hacking at D? But more seriously, what it's the status of the borrow checker? Also what's the status of the new C array proposal?
- WalterBright 3y agoI work on D, exercise, work on D, go for a run, work on D, go to the store, etc. We've decided to put new features on hiatus for the time being and focus on user support.
- trollerator23 3y agoThat's awesome
- gamache 3y agoNo questions here, just wanted to stand up as another person who still keeps holy the letters I through N.
- arunc 3y agoI'm a D expat using and managing Rust now. Do I miss D, yes. The D syntax is so beautiful and easy to follow. D code was always plastic, except there's just too many attrs @safe, @system, @oh-god-what-next that don't play well with each other. However, the runtime - not so much. I didn't complain much. We rewrote all of our core utils written in D (and many in C++) to Rust. No more null. Yes it took a while to onboard everyone to Rust, but hey, there's been zero production downtime since we migrated. Crazy compile times with Rust (compared to D), but we are happy with production runtimes! Rust is what D should've been, but it's crazy verbose! I can live with that and I do love Rust.
- WhereIsTheTruth 3y ago> D code was always plastic, except there's just too many attrs @safe, @system, @oh-god-what-next that don't play well with each other. Rust has the same problem, kinda, `#[derive(Debug, Clone)]` spam everywhere for example.. I think D can still catch up, Enum/Tuple/Option are relatively easy to implement, competition is growing however, even for Rust I much prefer nullable in Zig for example