10 ms·
Is your programming language unreasonable? (2015)
- tempodox 6y ago(2015)
- deleted 6y ago[deleted]
- gitgud 6y ago> "4. Objects must always be initialized to a valid state. Not doing so is a compile-time error." So this essentially means there can never be a NULL object assigned to anything... how is this possible? is it avoided by throwing exceptions instead of returning NULL?
- bollu 6y agoYou use sum types / optional<T> / Maybe / Option<T> / whatever your language calls them. Such a value can have two states: 'None', where it calls no value, and 'Some(t)',where it carries a value 't'. For example, a division function could return (1) 'None' for 'div(20, 0)' because division-by-zero is undefined, and return (2) 'Some 10' for 'div(20, 2)' because 20/2 is defined and is equal to '10'.
- mbogun 6y agoThree is a great article about option tyle in the context of F# here: https://fsharpforfunandprofit.com/posts/the-option-type/ https://fsharpforfunandprofit.com/posts/the-option-type/
- danielinoa 6y agoNot quite. There can be a null value if the object type allows for it, think Optional<T>. What you can't have is an undefined object, as you could in Javascript for example.
- Smaug123 6y agoIf you forget everything you ever learned about programming, and then I ask you for an integer, you're never in a million years going to reply "null"; the existence of "null" is something you had to learn. A language could simply not have a concept of "null". Even an OO language could have no concept of "null". In practice, it's useful to be able to answer "I'm sorry, Dave, I can't do that". Some languages do that by silently and implicitly allowing this special value "null" in a return type. Some languages allow you to throw an exception (again without ever having declared that this is something you might do). But some languages take one of those approaches and make it explicit: in the two cases, you have the "option"/"maybe" type, or checked exceptions. Either of these explicit methods let a function declare that it may fail, and moreover the language forces you to handle the possibility of failure in any function which may fail. The "reasonability" comes from the fact that the language forces you to be explicit about the possibility of failure, rather than implicitly allowing all functions to fail.
- rightbyte 6y agoNull is just an invalid pointer target that you can verify easely. It is just abi convenience and removes the need for a flag. On the other hand I unironicly write goto:s and use globals as much as a please (think GNU Bison/Flex) so who I am to judge.
- lmm 6y agoAt ABI level you might want to use all-bits-zero to represent something other than a valid instance of your type, sure; that's how Rust's option type works. But making your language semantics say that that has to be allowed everywhere a valid instance could be passed is a mistake; for most functions it's an error to call that function without passing valid argument values, so it's better if the language has a way of enforcing that rather than forcing you to write it out by hand every time.
- rightbyte 6y agoYe some "always valid kinda a pointer"-type is nice. I wish there was some standardized compiler support for that in C, like MS kinda silly empty macros in function signatures to annotate input and output pointers etc.
- nullsense 6y ago>So this essentially means there can never be a NULL object assigned to anything... how is this possible? I believe Crystal doesn't let anything be null either. It's quite interesting.
- tremon 6y agoNot only that, it implies that there must be a valid null-state for every type, which is also a bad idea.
- jackcviers3 6y agoNo, it implies that there must be a generic two-valued datatype to wrap every state: Just t | None You can have your null case, and avoid the null-pointer exception, default keyword, chained ? null-check hellscape in which we currently reside.
- sullyj3 6y agoI'm not entirely sure what you're getting at here. Are you saying the author is arguing a call like new Customer() should return something valid? I think the FP way would be to disallow constructor overloads with no arguments, as really, there's no reasonable thing to be done here. If you don't have all the necessary information to initialize a customer yet, you don't get to initialize one, and you pass around a partially applied constructor function instead. ie, The argument is that (in pseudocode): Customer(name = "Jimmy", address = null) is not really a customer if address is a necessary piece of information, so we should distinguish at the type level by passing around a function that takes the address and returns the correctly initialized Customer like so \addr -> Customer(name = "Jimmy", address = addr) Admittedly, this prevents you from accessing name until you provide an address, but I don't think there are many valid use cases where you want to access data before the Customer value is corrrectly initialized. I'd consider this a code smell to be refactored.
- learc83 6y ago> Admittedly, this prevents you from accessing name until you provide an address, but I don't think there are many valid use cases where you want to access data before the Customer value is corrrectly initialized. I'd consider this a code smell to be refactored. If you really needed to do that you could just define a partial customer with no address field and pass are that around.
- lmm 6y agoNo it doesn't; the whole point is to avoid every type having a null state. You only define null states for those types for which it's semantically valid. For most types, you just don't have a null state and don't allow them to be constructed in an invalid state.
- deergomoo 6y agoYou can still have “the absence of a value”, but it’s codified in the type system. Most languages with this feature implement an Optional or Maybe wrapper type, that is either Some<T> or None (i.e. no value). The difference is, because you’re now receiving a different type, you are forced to deal with the case where there’s no value. Rather than your function returning a Customer that could sometimes be null—and if it’s only null infrequently, you’re much more likely to forget to handle it—it instead returns Optional<Customer>. To operate on that Customer object, you are forced to unwrap the Optional by checking whether the underlying value is there. How well you handle the None case is up to you, but the compiler won’t let you forget (generally anyway; for example Swift allows you to force-unwrap optionals with a crash on the None case, but eh).
- dudul 6y agoYou can represent the possible absence of value with your type system. You can use a Maybe/Option type for example.
- masklinn 6y ago> So this essentially means there can never be a NULL object assigned to anything… No, it means that you can't leave bits off yet claim to have a valid object, either the object is fully initialised or it's illegal. This is mostly a concern in older languages like C where you can reserve space for a struct then not fill in anything. In some more recent languages (Java, Go, C# historically though that's changing) everything is zeroed by default so you avoid the "random garbage" situation, but it means you need to assume anything can be zero even if that's not intentional, and depending on the specifics you might "double write" each location even when that's not actually necessary. In the more functional slate of languages, you simply can't let any members off. There might be a concept of "default value" but that tends to be opt-in, the baseline is that if you define a type you must fill in every member, explicitly, before the compiler will accept it. Initialising items to null is orthogonal. Now since the rest is predicated on a misunderstanding I've a version of each for "incomplete initialisation" and a version for "null". > how is [forcing complete initialisation] possible? The compiler just requires that every member of the structure be specified. > how is [things never being NULL] possible? Don't make NULL an implicit member of every type. > is [incomplete initialisation] avoided by throwing exceptions instead of returning NULL? it's avoided by the compiler refusing to compile your code if you leave off members. > is [things never being null] avoided by throwing exceptions instead of returning NULL? It's avoided by NULL being a member of a completely different type than anything else, whether that type is a special case or not e.g. in Rust "null" is one member of the Option "wrapper type" so to indicate that, say, a user is optional you write `Option<User>`, then that can either be `None` (no user, ~ null) or `Some(user)`. If you just type something as `User` then it's necessarily a valid user and nothing else, because there is no null value you could put into it, you'd get a type error. * Rust playground which fails to compile because one of the struct members was left off: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=560e1ea6c9b767ec9ca6a5f913b955aa https://play.rust-lang.org/?version=stable&mode=debug&editio... * Rust playground which fails to compile because trying to put a null (None) in a non-nullable field: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=4dede06ad070f65e525dfd16c423223f https://play.rust-lang.org/?version=stable&mode=debug&editio... * Version of the previous with an optional (nullable) member: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=a741aab6d7000320c1c5fe736fbb2ef3 https://play.rust-lang.org/?version=stable&mode=debug&editio... * Version with opt-in default (through Rust's Default trait, the default derivation of the Default trait will recursively default each field, which usually means some sort of zeroing): https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=ba0aa527cf597e0abbc250dbabb4b3c3 https://play.rust-lang.org/?version=stable&mode=debug&editio...
- rootsofallevil 6y ago> I don’t care what my language will let me do, I care more about what my language won’t let me do. I think this is the best summary of the article. In short, please protect me from my own stupidity and/or laziness.
- sullyj3 6y agoI like how this could be taken as an endorsement or criticism of the article, depending on the hubris of the reader
- deleted 6y ago[deleted]
- mpweiher 6y agoOr the hubris of the language designer.
- mwcampbell 6y agoI would only take it as hubris or condescension if the language designer chose to write their compiler in a language other than the one they were designing. Otherwise, I'd interpret the presence of strong guard rails as humility, an acknowledge of how fallible our puny human minds are and how much help we need from machines to minimize the cost of our inevitable mistakes.
- rytill 6y agoI find that these sorts of polarizing statements which either side can take to mean a one-up on the other side tend to get lots of upvotes on applicable forums.
- mpweiher 6y agoAlan Kay referred to this as the "Wirth school of non-programming". It obviously has some merit, but I'd rather have a language be an enabler rather than a disabler. And again, not having to worry about messing up is or at least can be a kind of enabler, but I prefer a more positive approach, with good defaults encouraging me to do the good and simple, but the language getting out of my way for things it might not know about.
- rjakobsson 6y agoAccessibility (as in execution environments) trumps language features in my book, and JS is the most accessible language there is. But of course, it all boils down to what you're trying to solve and where your program will live.
- deleted 6y ago[deleted]
- mannykannot 6y agoIt is very likely that JS is the most accessible language in terms of execution environments, but it is not so as a consequence of the issues that are discussed in this article.
- rzwitserloot 6y agoI lost the line of reasoning (or perhaps the author did) at the second example. If I have 2 InputStream objects, nevermind equality. It just doesn't apply here. I don't even want `a.Equals(b)` to be considered valid code in the first place. Having __any__ default behaviour here is a lot like the permissiveness that is bashed in example 1 (a trifecta of unexpected variable scoping rules, the ability to re-type variables merely by assigning an object of a different type to them, and the idea that a boolean will silently convert itself to a numeric value): There are a billion situations where you'd want to e.g. treat a boolean as a number, and in the vast majority of them, '0' is a fine value, but nevertheless, it leads to unpredictable code so let's not default. Let's require an explicit interaction from the code author to indicate they really want to do that, and, hopefully, we can make that 'explicit interaction' simple. Apply the same logic to example 2 and the author draws the wrong conclusion.
- Smaug123 6y agoNote that F# has the `[<NoEquality>]` attribute for exactly that sort of case.
- masklinn 6y agoThat's the wrong default though. There is no reason to assume equality is a sensible concept, so objects should not even be equatable by default, and you could have an attribute which gives them structural equality (with a third option of a full-blown custom equality if necessary) as e.g. Haskell or Rust handle it.
- joshsyn 6y agoAlzabraic types and records have strutural equality while classes don't have it I believe.
- tome 6y agoI don't understand how you concluded that Example 2 implies that the author thinks that `a.Equals(b)` should work on InputStream objects.
- mitchtbaum 6y agoSpeakeasy, Lightouch, Jazz is a fifth-generation programming language. I find only small hurdlesv when showing it to programmers. My challenge now is mainly getting stable shelter so I can compile these notes and hand them off to people.
- ColinWright 6y agoI really can't work out what you're trying to say here. I can guess, but maybe it would be better if you wrote something more expository or discursive. For example, is "Speakeasy, Lightouch, Jazz" intended to be a single noun? A single thing? Or is it a collection? A sequence? What do you mean by "getting stable shelter"? Or to "hand off notes to people"? Have you written something already? is there a link?
- dunefox 6y agoI'm assuming that comment was written by a bot, tbh.
- adwn 6y agoJudging by that user's other comments, you're probably right.
- ColinWright 6y agoAs it happens, it wasn't, it's a real person.
- mitchtbaum 6y agoYes, I am real
- Memosyne 6y agoI think he's referring to this: https://github.com/speakeasy-engine/lighttouch https://github.com/speakeasy-engine/lighttouch
- sullyj3 6y agoPrevious discussion (2015): https://news.ycombinator.com/item?id=8998117 https://news.ycombinator.com/item?id=8998117
- gridlockd 6y ago"Objects containing the same values should be equal by default." No. This breaks down as soon as you have references to other objects in your objects. If you compare by reference, you probably don't get what you want. If you compare by value, you need to go arbitrarily deep, which is not a sane default, because of possible reference cycles. You need a distinction between objects and value types. C# has that (record types). "Once created, objects and collections must be immutable." If this is how you want to program, fine. Don't tell me this is "the right way" to program. Immutable data structures can have a lot of overhead, which disqualifies them from many domains. The real world is all about mutable state. At some point, you need to take the training wheels off.
- dudul 6y agoThe whole point is that equality should have nothing to do with comparing references. Your comment is just making the author's point. References, cycles, mutable state these just make it harder to reason about your code. And a nice petty little zing at the end for good measure. Good job.
- tsimionescu 6y agoThe point they raise is valid, though the tone is lacking. If you want to define a graph data structure, cycles are inherent, so there must be some way to deal with them - they are not something that can simply be ignored. This is a completely practical problem, and identity instead of value equality is a valid answer. Of course, this doesn't contradict the original article: reference equality can still exist in the language, it just helps if it's not the default. For example, in Common Lisp you have both `eq` (reference equality) and `equal` (typed value equality) [and even the somewhat stranger `equalp` (structural equality, e.g. (equalp '(1 2) '(1 2)) -> true)].
- lysium 6y ago> The real world is all about mutable state. At some point, you need to take the training wheels off. Funny enough, I think the opposite is true. For example, if you have a moving point in the real world, there is never a point where only the x coordinate is mutated, yet you have that in your mutating code.
- rishav_sharan 6y agoA bit of a tangent, but I tried F# recently to make a small gamedev framework. I absolutely adored the language at first. Loved the syntax. loved the type definitions and domain modeling. I felt maybe, this is it, this is the language for me. Never felt that way about a language since the last time I wrote something in Crystal. Then came the part where I needed to work with list of records and realized that something as basic as that which I take for granted in other languages, is a monstrously complicated concept in f#. I got introduced to the shady world of Lenses, tried my hand for a bit and decided to get out. Lenses are not for me. I can't believe that a language makes updating records so difficult. F# is my first functional language and while I expected some friction in doing such stuff in a functional language, I never expected so much work around something so trivial. But is does helps me understand why people don't use F# for things like gamedev. Anyway TLDR; love the language. Wish that it will make concepts like Lens much easier to use in the language
- retendo 6y agoCould you go into a little more detail what task you were trying to solve?
- rishav_sharan 6y agoIts been a while, so I might be saying it wrong but, I was trying to create an entity collection. Essentially, a super basic scene graph. on every tick in my update call, i would update something about a specific entity in that collection (maybe position, transform etc). And then in my draw call I would render the entire updated collection. Just getting a specific entity from the list was a huge issue and then updating it and putting it back was worse. I gave up after a many days of trying something as basic as this. In JS, Nim, Crystal etc. this would have taken me a couple of lines of code. Here I ended up reading bid docs on something called Lens and was never able to apply it correctly. here is the repo i was working on https://github.com/rishavs/coral/blob/master/Engine.fs https://github.com/rishavs/coral/blob/master/Engine.fs
- rtlfe 6y ago
- Lavinski 6y agoThe article goes through a series of examples to show motivations for the following: 1. Variables should not be allowed to change their type. 2. Objects containing the same values should be equal by default. 3. Comparing objects of different types is a compile-time error. 4. Objects must always be initialized to a valid state. Not doing so is a compile-time error. 5. Once created, objects and collections must be immutable. 6. No nulls allowed. 7. Missing data or errors must be made explicit in the function signature. The idea being that each feature or constraint _enables_ you to reason and predict more about a program than you could otherwise. I encourage anyone interested in these ideas to play around with F# or a similar language and get a feeling for how they influence your code. If you've mastered one paradigm such as OO one of the best ways to find holes in your mental models is to try and find another point of view to look at the same problems. Even if you keep writing most of your code like you do today, in the language you do today, it can still be beneficial.
- nyanpasu64 6y agoRust's borrow checker would prevent Example 5 from compiling, since once you add `cust` to the collection, you can't touch it anymore (unless you insert a clone or etc.). So in this case at least, the inability to reason about code can be resolved by banning mutable aliasing, without eliminating mutability.
- masklinn 6y agoRust's ownership system would prevent example 5 from working (because you have to move the instance into the set). The borrow checker is about validating that references don't outlive their target, and R^W.
- nyanpasu64 6y agoOops.
- leephillips 6y ago5. Once created, objects and collections must be immutable. So this language would not be general purpose, as it would not be suitable for high-performance computing. Large scale simulations almost always involve arrays that are modified in place. Being able to somehow declare a collection to be immutable would be highly useful, but not having the option of mutable collections limits the kinds of problems that can be approached with the language.
- andi999 6y agoI thought if you need to reason about the code you use Ada with Spark?
- alkonaut 6y ago> Objects containing the same values should be equal by default. I strongly disagree with this. I’d take it one step further and say that there should be no equality operator or default equals function for non-primitive data types. Some specific type of records/tuples could use a default equality, but it should be opt-in or follow from using metadata like an attribute (e.g Rust derive) or only for those specific types (e.g C# ValueTuple) . Not even strings should have a default equality (ie it shouldn’t be possible to create a map/dictionary with string keys without explicitly also saying how keys are compared. That’s right: It shouldn’t even be an optional argument so the default string equality/hash is used unless specified. It should be explicitly passed. This isn’t “boilerplate” it’s avoiding subtle bugs. As for the rest of the arguments, some are downright crazy. You cannot program without mutation in the general case. You simply don’t always have the performance/power budget for it. Obviously mutation should be avoided, but dogmatically using all-immutable data is just not possible. No automatic casts would be high on my list too. I don’t ever want to accidentally treat things like “falsy” or convert between types based on usage. I’d rather litter my programs with type names and explicit conversions.
- lostcolony 6y ago'You cannot program without mutation in the general case. You simply don’t always have the performance/power budget for it.' So by 'general case' you mean 'every case'? Because to my mind, those instances you don't have the performance/power budget for 'it' are very much special cases, not general ones. It also seems a curious distinction; I mean, Python and Ruby both allow for mutable data, but are quite slow and memory intensive. Haskell doesn't allow mutability, but is quite fast, and also surprisingly memory efficient when written well. What languages do you consider to be able to address 'the general case'?
- alkonaut 6y agoI mean for a programming language to be able to handle the general cases it should also handle the maybe 10% or 20% of programs that might need mutation in their hot paths. Obviously one solution is to call out to a different programing language for only the areas that require it, but I’d consider that a definite deficiency if it’s commonly required. I think e.g C# and F# handle mutation well, but I think F# does the right thing by not encouraging it like C#. Being able to control the in-memory representation of data and do things like sort arrays in-place etc to some people seems to be rare and to others happens on day 1 even at toy scale. Depends on what problems you are solving I suppose.
- mwcampbell 6y agoAmong reasonable programming languages (as defined in this article), which one compiles to the most optimized JavaScript that can efficiently interoperate with external JS code? Reason or OCaml via BuckleScript? F# via Fable? Scala? Maybe Kotlin is close enough?
- scns 6y agoI would bet my money on Bucklescript/ReasonML/ReScript. https://github.com/rescript-lang/rescript-compiler/wiki/Why-bucklescript-matters-for-Javascript-platform https://github.com/rescript-lang/rescript-compiler/wiki/Why-... https://www.javierchavarri.com/performance-of-records-in-bucklescript/ https://www.javierchavarri.com/performance-of-records-in-buc... Its Standard library Belt is optimized to compile to efficient JS too. (edit: added line about Belt)
- qsort 6y agoIs Reason even actively developed these days? Isn't it sort of a dead project, or have I misunderstood something?
- mwcampbell 6y agoI just discovered that the BuckleScript team started a rebranding to ReScript a few months ago: https://rescript-lang.org/ https://rescript-lang.org/
- qsort 6y agoYeah, that's the issue with this kind of tools, unfortunately. I myself would like a more functional-oriented approach, but realistically JS/TS is a more stable ecosystem. My go-to choices are JS for small individual projects and TS for larger projects or if a team is involved.
- toolslive 6y agoit's all about surprises. Programmers hate them. You want to be able to reason about your code For example: fact: a == b fact: f(x) is p deterministic pure function. So I figure f(a) == f(b) Well, maybe... Javascript (obviously) and Python are amongst the programming languages where the above reasoning is not true. (there are exceptions).
- masklinn 6y ago> Well, maybe... Javascript (obviously) and Python are amongst the programming languages where the above reasoning is not true. (there are exceptions). There are lots of exceptions. For starters (2) seems like an assumption rather than a fact. An other assumption which is just that is that equality is transitive through f. This is not a generalised fact in any language which allows overriding equality.
- masklinn 6y ago> An other assumption which is just that is that equality is transitive through f. This is not a generalised fact in any language which allows overriding equality. Simple example: hashsets equality & conversion to sequences. Hashset equality, to the extent that it's present, is always based on the presence of items in the set. But the iteration order of two hashsets with the same contents can depend on the way they were initialised (as that influences whether collisions happen) and the order in which items were added. So given a and b be hashsets and f be toArray (or whatever), it is very common to have a == b but f(a) != f(b).
- toolslive 6y agoeven with the python `int`s a: int, b:int and the `str` function as the pure deterministic function you still cannot make the conclusion. It's an utter mess
- z3t4 6y agoI agree that the more stuff the language lets you do - the more people will abuse it. But there is also a productivity of higher level languages.
- tzs 6y agoIn example #6, he gives this as the unreasonable approach: var repo = new CustomerRepository(); var customer = repo.GetById(42); Console.WriteLine(customer.Id); with the issue being customer can be null, which is not being accounted for. The reasonable approach he says is to use a sum type: var repo = new CustomerRepository(); var customerOrError = repo.GetById(42); if (customerOrError.IsCustomer) Console.WriteLine(customerOrError.Customer.Id); if (customerOrError.IsError) Console.WriteLine(customerOrError.ErrorMessage); Do most (or any) languages in which people take this approach actually enforce handling of all cases? Or could a programmer write that this way: var repo = new CustomerRepository(); var customerOrError = repo.GetById(42); Console.WriteLine(customerOrError.Customer.Id); and still have the same problem as the original? It seems to me that the "reasonable" version is getting its reasonableness from naming the variable that gets the GetById return "customerOrError" which reminds the reader that there is an error case, not from the language having sum types. That's just a convention. Nothing stops you from naming it "customer" just like in the "unreasonable" language. (I'd actually expect that from a lot of people, because you are probably going to have a lot more code in the case that you have gotten the Customer variant than in the Error variant case, and you probably don't want to be calling it customerOrError in the 99% of the code where you know that it is a Customer).
- isaacabraham79 6y agoNo. In languages like F#, you can encode in the type system that the result can be either a customer or an error. And the compiler won't let you "access" the customer until you've safely checked that the result is, indeed, the customer rather than an error. In this way you end up encoding more intent within your types and behaviours rather than implicitly letting the consumer figure it out themselves.
- whoisburbansky 6y agoAdding to this, it's nice to have the compiler enforce these things rather than rely on the dev to see the variable name and treat it accordingly. In the latter case, you're one sleepy afternoon away from having things blow up in production because you "thought there was no way that that variable could be a null pointer"
- amai 6y agoSo which languages apart from F# are reasonable according to the authors requirements?
- luord 6y ago> The fundamental paradigm of OO (object-identity, behavior-based) is not compatible with “reasonability”, and so it will be hard to retrofit existing OO languages to add this quality. This is false, I knew this was false even without reading what he meant by "reasonability" and, upon reading what he meant, I confirmed that it is, indeed, false. Something that I too found odd is the separation between "functional" and... "Mainstream". That got me confused, LISP is over sixty years old and anybody with more than a passing education in computer science knows about it; there are few languages more mainstream than LISP. I just don't understand this flamewar; terrible code in Clojure is no easier to read than terrible code in Python. Conversely, good code in either language is no easier to reason about than good code in the other.