|
Just started reading this (just released) book, The C# Type System (no starch press)[^] and the first chapter is kind of blowing my mind.
Step 1
Author starts out with the following example and says,
"You must use better named variables so dev users know what they mean."
Displacement(double t, double v, double s)
{
var x = v * s * Math.Cos(t);
var y = v * s * Math.Sin(t) - 0.5 * 9.81 * Math.Pow(s, 2);
return (x, y);
} Yes, that makes sense.
Step 2
Then he says,
"Oh, you can add meaning with this new idea of named arguments so users don't have to remember order that they should be passed in."
var result = Displacement(angle: .523, speed: 65, elapsedTime: 4); Ok, yes, that is good advice with the modern capabilities.
Step 3 May Blow Your Mind
He mentions that the code is still confusing because all three arguments are the same primitive type (double) and this leads into...
From the book: Primitive Obsession code smell, which describes any code that has an overreliance on primitive types—that is, those types that are built into the language, such as int, double, and string.
The solution is...
Wrap All the Primitive Types In Structs 🤯🤯🤯🤯🤯
public struct Angle
{
public double Size {get; set;}
}
public struct Speed
{
public double Amount {get; set;}
}
The Paradigm Has Shifted
Now, when the user attempts to call the Displacement method the compiler will know that the argument type is wrong.
Now, there's no way to pass the wrong value into the method, because the compiler will know the type.
Wow, that is a very different paradigm!!
Step 4 Is Immutability
Now, make each struct immutable so that it cannot be altered after construction.
public readonly struct Speed
{
public Speed(double amount)
=> Amount = amount;
public double Amount {get;}
}
I began learning OOP back in 1991 or so and it was much different then with a strong focus on Inheritance. Of course we all learned the pain of inheritance and then the Gang Of Four said,
"prefer composition over inheritance" and that changed a lot of thought on OOP.
Design to a interface and not a implementation.
It is interesting now because there seems to be a switch from Class-focus (heap-based objects) to Struct-focus (stack-based objects) at this current time.
Keep in mind that when Java was created that the designers literally made everything a Class.
I mean, basically C# is that way too with Object at the bottom.
In iOS / SwiftUI they have officially said, "If you create a new thing then create a struct not a class" and they explain why.
What Are Your Thoughts?
Anyways, what do you think about this "Primitive Obsession code smell"?
I see the value in it -- and I'm assuming that the people promoting this are saying do this for specific things in the domain and not all of them.
I cannot image wrapping all primitives in structs and having that many extra types. It feels odd but I can definitely see the value / benefit. But it's really odd after all these years.
|
|
|
|
|
I agree that Primitive Obsession is a code smell. It can easily cause bugs when, for example, a plain integer is interpreted as a time duration. Was that in seconds? Milliseconds? Microseconds? There have been articles about the topic on this site. Here's one of them, but in C++:
Units and measures for C++ 11[^]
Fixing that code smell in a legacy system can be quite a bit of work.
I don't subscribe to the "prefer composition over inheritance" mantra when stated that simply. If B is a type of A , B should derive from A (inheritance). But if B only has an A , but also other behavior that has nothing to do with an A , B should contain an instance of A (composition). The problem with using composition when inheritance is called for is that B ends up cloning most of A 's interface and forwarding to it.
|
|
|
|
|
Greg Utas wrote: It can easily cause bugs when, for example, a plain integer is interpreted as a time duration. Was that in seconds? Milliseconds? Microseconds?
Yes, I agree. That is a very good example of where this struct wrapper would be highly beneficial.
And, I do like the discussion around "using more structs". It is just interesting that in the past, it was "everything is a class" and to see that re-thought.
|
|
|
|
|
In C++, there's no difference between class and a struct , so that comparison with C# is likely to be misleading. In C++, the two keywords often have different semantics by convention, but they're the same to the compiler. If you look at the STL's <chrono> , there's no way to confuse a duration in seconds with one in milliseconds. Their object wrapper precludes it.
|
|
|
|
|
raddevus wrote: Anyways, what do you think about this "Primitive Obsession code smell"? It seems like the book author is someone trying to sound smarter than they are. Just because someone writes a book doesn't make them a genius. Now, I do agree that primitive obsession is bad, but so is struct obsession. A struct won't inherently prevent a coder from mistaking milliseconds for seconds (to borrow from this thread's example). But, what it does do is offer more complexity in an application that may otherwise not be needed.
A lot of these "new" ideas are just rehashed old ideas from JavaScript. I'm dead serious. It's just people looking for something to do rather than go outside. In JavaScript, some folks love to use an object as a parameter for everything. It's the loose and fast version of a struct in C#. It's just as ridiculous to expect an object as a single param in every last routine.
The problem is, the obsession or abuse of any one concept. Average coders take one little thing and run with it because it's the new shiny doodad. Abusing structs is no better. It's just change for change's sake while pretending to be smart. It's about balance.
raddevus wrote: Step 4 Is Immutability To the point of the book, to make each struct read only is a good idea. But, to the point of "prefer composition over inheritance", both Java and C# were literally designed with an OOP paradigm in mind. Move to a functional language if you want to start acting functional.
In regard to immutability, you can use a sealed class in Java and C# as well.
The irony is, all this struct talk is reminding me of C. People always said C sucked because it doesn't support classes. And yet, here we are. People just following the hype train because people looking to change something for no real gain and refuse to go outside. And I say this as a dude who loves functional programming, C# wasn't designed that way.
Jeremy Falcon
modified 22hrs ago.
|
|
|
|
|
That was a great read. I had many of the same thoughts as I originally read the material.
Jeremy Falcon wrote: The irony is, all this struct talk is reminding me of C. People always said C sucked because it doesn't support classes. And yet, here we are. People just following the hype train
Agree 100%!!
Thanks for being so direct in your post. I wondered if there were others out there who thought the same thing. It's interesting that so many things have been done in the past and then along comes someone who says, "Hey, I got this shiny new thing."
I do see the benefit of this used in small special cases though. But, of course, once a certain set of people read about "Primitive Obsession" they will be obsessed with wrapping all primitives in structs. It's the latest shiny thing, after all.
|
|
|
|
|
Any time man. The longer you're in the industry, the more you see these patterns emerge over and over and over.
Here's another example, in the 90s when MS started pushing XML like crazy. Average coders were all like "omg clear text file formats". But, a decade prior we had SGML that did the same exact thing. SGML also required a DTD. Sure, XML was more strict... but it was nothing new in concept. And all these kiddies came along acting like they cured cancer because binary file formats was baaaaaaaad.
Even made its way to the web. XML this. XML that. But it was nothing new. Fortunately, the web eventually said screw that... it's bloated. Now JSON is the defacto non-official standard in the web world, and we use conventions rather than a DTD.
But the point is, if you used SGML for your typical file format in the 80s... I guarantee you coders would've came along saying how stupid that is.
Jeremy Falcon
modified 21hrs ago.
|
|
|
|
|
The SGML v XML discussion is a perfect example of it, for sure.
And speaking of JSON -- the detractors for JSON want this new, new, new THING!!
(RUST uses it)
It's called TOML (Tom's Obvious, Minimal Language[^]).
TOML is so amazing!! It's so new. It's never been seen before!!!! Squeeeeee!!
Oh, wait, Windows 3.1 Ini files[^] used that same format. 😅😆🤣🤓
|
|
|
|
|
raddevus wrote: Oh, wait, Windows 3.1 Ini files[^] used that same format. Ha ha ha ha. That's so true. In fact, one of the reasons I haven't really decided to learn Rust yet is it's too opinionated.
Jeremy Falcon
|
|
|
|
|
Jeremy Falcon wrote: in the 90s when MS started pushing XML like crazy. It most definitely was not limited to MS! I never even expired them as very dominating in the XML rush. It was always the *nix guys who insisted on all information in the system being stored as 7-bit ASCII so any hacker would be able to modify any system data using ed for modifying it.
To pick one example: Open Office XML entered the market several years before MS changed from binary formats to OOXML.
I never loved XML (and some of the tools even less than the plain XML format, e.g. XSLT). I went to a Digital Library conference around 2002, and for the first twelve presentations I visited, eleven of them made a major issue of their use of XML and all its benefits. Methinks not. But the issue was not up for discussion; it was settled, carved in stone: "XML is good for you! Always!"
After XML we went into a period of 'Every decent programmer has his own data description language'. One project I was in - and it wasn't a very big one - used four different description languages, all of which covered the same functionality. If JSON is the one to kick out all the others, it is a step forward, no matter its weaknesses. But it seems like new alternatives keep popping up all the time. Maybe JSON is The Answer this year, but I am fully prepare for it being thrown to the wolves within a couple of years.
|
|
|
|
|
Jeremy Falcon wrote: The irony is, all this struct talk is reminding me of C. In my first University level programming class, Pascal was the programming language. We were taught a programming discipline where all attributes relating to a phenomenon (such as a physical object) where put together in a struct. All manipulation of variables of a given struct type were done by a set of functions and procedures (those are the Pascal terms) declared together with the struct type definition. All functions / procedures should take a pointer to a struct as its first argument.
The course included creating general struct types, and including these in more specialized substruct types adding more attributes. It also included handling polymorphic types, using the variant structure facility of Pascal.
I took this first course in object oriented use of Pascal structs (no, we didn't label it as such!) in the fall of 1977.
When OO arrived (C++ in 1985), we moved the first argument - the struct pointer - to make a prefix to the function / procedure name, with a separating dot. The biggest change was the OO term, and starting to say 'method' rather than function / procedure. Sub/superclasses, polymorphism and a general OO mindset had been in place for several years; we just didn't know it by that name.
So quite fancy use of structs for creating objects / blackboxed types has at least 45 years on its back. Back then, some of it relied on programming discipline, not compiler support - but use of structs to create distinct types, as the book author suggests, doesn't really have any compiler support at the concept level, either.
|
|
|
|
|
Extremely fascinating story. Thanks for sharing.
As I was reading your description...
trønderen wrote: All manipulation of variables of a given struct type were done by a set of functions and procedures (those are the Pascal terms) declared together with the struct type definition. All functions / procedures should take a pointer to a struct as its first argument.
...I was thinking, that sounds like a class (or just a half-step away) -- data encapsulation with associated functions that work on the data. Very interesting.
trønderen wrote: Back then, some of it relied on programming discipline, not compiler support
I have talked about this for a long time.
1. If you don't have disciplined devs (engineering mentality of "do the right thing"), then
2. you better have a technology that forces the discipline (example, private vars cannot be manipulated outside class).
This is also why old timers (who had to have a disciplined mindset so they didn't cause themselves problems) see a lot of the new stuff as just fluff.
Two Thoughts
1. There are people who still create total crap, even with all the tools and automated discipline we have now.
2. There were people in the past who created amazing feats of software, even though all the discipline was required to be inside them.
|
|
|
|
|
Quote: This is also why old timers (who had to have a disciplined mindset so they didn't cause themselves problems) see a lot of the new stuff as just fluff.
I resemble that remark.
"They have a consciousness, they have a life, they have a soul! Damn you! Let the rabbits wear glasses! Save our brothers! Can I get an amen?"
|
|
|
|
|
There were languages (in this case, CHILL might be an example) that allowed you do define incompatible types ('modes' in CHILL lore):
NEWMODE Weight = FLOAT, Distance = FLOAT;
DCL amountOfApples Weight, LondonToNewcastle Distance;
amountOfApples and LondonToNewcastle both have all the properties of a floating point value, but they cannot be added, multiplied, compared, ...
This is a much simpler and more obvious way to get that protection the book writer is aiming at. Implementing it in the compiler should be a trivial matter, and the runtime cost 0.0. Checking type compatibility between primitive types is pure compile time matter. (And I assure you: This one will not make any impact on compile time.)
General lament: There are so many grains of gold in old, phased-out technology. We should spend much more time checking if a problem already has an old, forgotten, but totally satisfactory solution, before we design a new one.
|
|
|
|
|
|
I've been working a lot lately with Spring Webflux/Reactor and they liberally use the Duration class for any time specs.
//so instead of
long ticks
long ms
long s
//etc, etc, you see
Duration t
//and you create values using stuff like
Duration.ofSeconds
Duration.ofMilliseconds
By not obsessing over primitives, they made it so that all methods that use times can accept any time. You don't have to constantly remind yourself what the context for that time value is (e.g. seconds, milliseconds, etc), because the method doesn't specify the context, you do. So I love the idea of better contextualizing values beyond their strict storage type. As long as there's a useful context that adds value.
From your example, I think an Angle abstraction that handled both radians and degrees could prove useful in a similar manner to Duration , for example. As given, I'm not sure abstracting a double to an Angle solely to remove the primitive is a good pattern though. My assumption is that the intention is to force the developer to explicitly contextualize the double value, but the thing is if the developer didn't care about the context before, they aren't going to care now. They'll just wrap the double they have and move on (e.g. ex.handleAngle(new Angle(someDoubleThatIsntAnAngle)) ). Elevating a primitive in this way doesn't actually achieve anything that variable naming and/or named arguments couldn't already do. Just having a nondescript Angle with a double size property does nothing to further describe a double angle parameter. There has to be more sauce to it to make the abstraction worth it in my opinion.
|
|
|
|
|
That's a nice example of a good use of creating types for the parameters and it makes sense.
Also, I'm just at the beginning of the author's example also and it seems he is taking the example much further so it probably isn't that the author is actually saying "well, just wrap all those primitives in structs" but is building the case for it as he continues his longer example.
I was just astonished to see this "newer" idea of wrapping primitives like that.
I will continue reading the book because it is making me think different and the author's point is to make "more readable" code too and any hints toward that always go a long way.
Thanks for your interesting post which really adds to the conversation.
|
|
|
|
|
Might wanna check my new, new reply. I'm being annoying in it.
Jeremy Falcon
|
|
|
|
|
Realistically though, how often do you need to contextual inputs like that? If it's external user input, it should always be sanitized first. Which means you can transform any exceptions in that layer. If it's internal user input, how often do you really change contexts like that in practice?
Don't get me wrong, nothing against structs as a param, but having said logic to handle the contextualization in every last routine that uses it (it's for inputs) isn't ideal.
I'd argue structs are useful for abstracting complex data types only, irrespective of the context in which they are called with.
Jeremy Falcon
|
|
|
|
|
I think it's similar to the old "bool-vs-enum" arguments that have been around for many years. An appropriately-named enum can certainly make the calling code easier to read than a bunch-o-bools, although it can lead to some interesting definitions[^].
Given how recent the book is, I'm surprised the author hasn't progressed to record structs[^]; perhaps that will come later?
public readonly record struct Speed(double Amount);
"These people looked deep within my soul and assigned me a number based on the order in which I joined."
- Homer
modified 4hrs 20mins ago.
|
|
|
|
|
He failed step 3. Angle is not very well implemented, as it does not specify what unit it uses. Either call it Radians, or specify the unit in the constructor and getter.
Ironically, this could possibly be implemented by inheritance. Angle being the base class and Degrees, Radians, Turns and Gons (optionally Longitude and Latitude) being the sub classes, although this would force it to be a class.
As implemented the client of this function could still create an angle of 90 (thinking degrees) and pass this in and get a point at 90 radians.
I agree that is is a good idea to make structs immutable. I have seen so many bugs caused by trying to alter a Point or Size but the code is altering a copy, not the original.
Is implementing structs for these a good idea? Possibly. I have implemented Angle as a class before - mainly for user display. I have also just used angleDegrees and angleRadians. Choose wisely.
The advantages are it forces the correct type to be passed in.
The disadvantages are a little bit of speed and extra code.
Would naming the angle as angleRadians be enough to eliminate most errors?
Do we need explicit casts on Radians so we can use (Radians)Math.PI?
Personally I prefer a plain double, named well, although an explicit type can be useful.
|
|
|
|
|
Edited: The original Subject here was 'The Day I Learned to Hate the BOM'. After considering the first response, I have modified it to reflect a more appropriate and less offensive title. I'm only explaining this to provide context for the first responder's grievance. (and strike tags are not allowed in the subject)
I've been importing text files for over 20 years and I thought I'd seen it all!...CSV lines enclosed in double-quotes, CSV fields with commas (not enclosed in double-quotes), implied decimal places, packed decimal format, subtotal lines, total lines, garbage lines, etc.
What I haven't seen before now are text files with byte order mark (BOM) characters that don't seem to do anything significant except to require dealing with. It's probably been an issue long before now, but 90% of the time, the files have a header row so it wouldn't have mattered.
For fun, or as an office joke, you can add BOM to your own text files by changing the Encoding in NotePad.
"Go forth into the source" - Neal Morse
"Hope is contagious"
modified 17-Sep-23 11:32am.
|
|
|
|
|
Is that the same day as you saw BOM for the first time?
What is wrong with BOM? That you don't want to see it at all? That for this specific file, it "isn't needed", because all it contains is 7-bit US-ASCII?
You may of course declare that "I handle 7-bit US-ASCII only, so don't disturb me with anything else - Unicode is not my business, even when the characters are limited to US-ASCII. Just bug off with anything else!"
Fair enough, but then the software you produce is not for me. Most likely not for very many people outside the English speaking world. You are most certainly expected to handle BOM and Unicode/UTF8 today. If you are handling CSV, your are supposed to understand that in major parts of the world, it is five comma three, not five point three.
For the other stuff you mention: Fair enough; that should be easy to handle. If you haven't yet discovered that there are lots of variations in CSV formats (e.g. they are certainly not always comma separated, like in cultures where the decimal separator is not the point), take a look at the import options in Excel: It has handled this stuff for at least 15 year - I'd guess a lot more. Accept reality as it is, not as if reality was a problem.
And so on. You are free to put up a sign at your door: "Warning: No non-American data is handled by our software!" Actually, I'd be happy to know in advance, rather than discovering after I have signed a contract.
|
|
|
|
|
Thanks for the reply and for allowing me to realize that I had mis-titled the thread. I understand that the word 'Hate' was inappropriate for a number of reasons, firstly because I despise that word, and secondly because of your point about reality.
In my defense, the 'problem' had already been dealt with, (code fixed, tested, compiled, tested, and deployed) for the two affected imports before I made that post. I was simply annoyed about spending that hour diagnosing and fixing something that I'd never seen before...weird characters trashing the first element in a csv.
Per your post, I had a few ways to deal with it, now having a crash course in BOM:
0: Tell the customer that they would have to change the encoding. (put up a sign) The customer gets these data files from other departments/systems. To change the encoding is would likely be a manual process. Not ideal at all.
1: Be aware that a text/csv file might have those three weird characters at the beginning of the file. If so, disregard the BOM and move on.
2: Go into full research mode and discover all that there is to know about BOM and how it might be needed for all of my non-US customers. (current/future === none)
I chose option 1. The reality is that I have to deal with it. The chances that I get a file from a US customer that is BOM encoded, and that does not have headers, and where the first element of the line is important have become realized. I didn't account for the BOM and got 'bitten'.
Anyhow, my customer is happy again. I've learned something new and improved my software.
One of my favorite things about this profession is the constant learning and improving.
"Go forth into the source" - Neal Morse
"Hope is contagious"
|
|
|
|
|
The only thing to hate is hate itself.
|
|
|
|
|