Who needs trigger warnings anyway? (Trigger Warning: trigger warnings)



  • Let us consider:

    a = 1
    

    In C/Java/C++/C#/etc, a has to be an existing thing - there is dedicated syntax for introducing new things into scopes, and it can't be done accidentally. In certain other languages, this just declares a outright. That's extremely dangerous: a simple typo is all it takes to make your code wrong and yet it compiles without errors, or in some cases it even compiles without warnings!

    I can understand older languages doing it: they didn't know any better. But [i]new[/i] languages!?[quote=Crystal]We want the compiler to understand what we mean without having to specify types everywhere.[/quote]So I asked about it in their IRC:

    [15:22] <LB__> When I assign to a variable, how do I know whether I'm declaring a new local variable or assigning to one in an enclosing scope? What if I want the compiler to warn me about the former?
    [15:28] <jhass> LB__: there's no difference in crystal, assignment and declaration is the same
    [15:29] <LB__> So if I make a typo the compiler won't even warn me?
    [15:29] <jhass> LB__: IME if you really have the problem of forgetting which local variables are valid in your current scope, you write way too long methods
    [15:30] <jhass> Crystal doesn't warn, either something is valid or not
    [15:30] <jhass> and of course accessing a local that was never assigned will error
    [15:33] <LB__> IMO that's a pretty bad thing. Oh well.
    

    I don't know about you, but every time I have had to spend multiple hours debugging code, it has been because of a simple typo that the compiler didn't warn me about. Some typos are pretty hard for humans to spot, but [b]it should be obvious to a computer[/b].

    Also, did you catch that?

    [15:39] <LB__> Wait, when you say "Crystal doesn't warn", do you mean there are never any warnings generated ever?
    [15:44] <jhass> no, only errors
    [15:45] <jhass> well, I guess we warn when you benchmark in non-release mode
    [15:45] <jhass> but that's like a stdlib warning, not a compiler warning
    [15:46] <LB__> That's going to be fixed, right? Surely there's some code that is obviously suspicious or wrong and the compiler can warn about it?
    [15:53] <jhass> LB__: if the compiler thinks something is wrong, it'll error out on it
    

    Ok, so warnings as errors is a good idea, but I think there are some cases where a warning is more appropriate. Then this guy literally spells out a perfect example:

    [15:54] <LB__> But it won't error out on typos?
    [15:54] <LB__> What about variables that are only assigned to and never used?
    [15:55] <LB__> I can get behind the 'errors only' thing, but only if it actually catches common mistakes
    [15:57] <jhass> erroring out on write only would be annoying as you temporarily comment out stuff
    

    This is the perfect case for having a warning. I couldn't have said it better myself.

    [15:58] <LB__> So why not have a warning for it? For instance, if you forget to uncomment that code or you make a typo?
    [16:00] <jhass> people ignore warnings
    [16:00] <jhass> you'll end up having to use a library that produces lots
    [16:00] <jhass> so you'll ignore them even more
    [16:01] <LB__> I guess we'll just have t agree to disagree.
    

    People ignore warnings? Well, yeah, but only if they're idiots. Just because idiots ignore warnings doesn't mean they shouldn't be there. As for the library argument...why would your brand new language allow warnings in library code to be displayed to the user of the library!?

    Crystal is not the only language with this problem. This attitude is exactly why I stay away from such languages. They are IMO scary dangerous languages where the community of developers doesn't afraid of anything.



  • Ruby-inspired syntax.

    :nope.mdb:



  • This thing is rather horrible. I mean, I've investigated it for like three minutes, and there's already a WTF of how they deal with array literals (arrays are strongly typed, but you can stuff anything anywhere into the literal, and the compiler will type the array as an union of all its components), who knows how many else lurk there?



  • Wow, arrays and tuples are one and the same? It's even worse than I thought.

    Also, I just found this:

    A Char represents a Unicode code point. It occupies 32 bits.

    I guess if you don't care about memory usage at all, this is a pretty good temporary solution.


  • I survived the hour long Uno hand

    compile-time [...] generation of code

    This is the same problem I have with Rails: I can't debug what I can't see.

    type-checked but without having to specify the type

    What if it guesses wrong?

    We want the compiler to understand what we mean without having to specify

    You want magic.

    An Array can have mixed types, meaning T will be a union of types, but these are determined when the array is created

    In what situation is this useful? It sounds like a fucking mess.

    name, age = "Crystal", 1
    
    # The above is the same as this:
    temp1 = "Crystal"
    temp2 = 1
    name  = temp1
    age   = temp2
    

    This sounds like unnecessary overhead.



  • @LB_ said:

    arrays and tuples are one and the same

    Worse. Say you have Arr = [1, "John", 0.5].

    If it was a tuple, you could put an integer under index 0, a string under index 1, or a float under index 2. Bad syntax, but manageable.

    Instead, it's an array of unions, meaning that you can put any of those three types anywhere you want.



  • Oh god.

    Let's assume it's being used in a useful scenario. So if someone happens to accidentally remove the one and only instance of a particular type...the code breaks and you have to look up the explicit syntax?



  • No, I assume the type is inferred based on the literal. It's gonna stay a (int/string/float)[], and you'll always be able to put an int, a string, or a float there (but not a Date, I mean you want strong typing, right?)



  • I meant removing the one and only instance of a type from the literal.

    Also, count how many times "special" appears on that page:

    Way too many times.

    EDIT: The union thing happens with hashes too!?


  • sockdevs

    @boomzilla said:

    http://crystal-lang.org/

    Ruby-inspired syntax.

    :nope.mdb:

    Ruby-inspired?



  • @LB_ said:

    ><jhass> people ignore warnings

    What he's really saying is that the language developers ignore warnings.



  • @LB_ said:

    In certain other languages, this just declares a outright. That's extremely dangerous: a simple typo is all it takes to make your code wrong and yet it compiles without errors, or in some cases it even compiles without warnings!

    Wanna hear something funny?

    In Python,

    def f():
        print(a)
    a = 3
    f()
    

    works as you might expect (prints 3), but this

    def f():
        print(a)
        a = 5
    a = 3
    f()
    

    just throws UnboundLocalError: local variable 'a' referenced before assignment.

    Because by assigning to a anywhere in the function, you are defining a local variable named "a". So now you can't access the global variable a in that function.



  • So basically it's passive-aggressive about shadowing variables? That's super weird.



  • @Maciejasjmj said:

    Worse. Say you have Arr = [1, "John", 0.5]["John", "3", "15.0"].

    FTFY



  • @boomzilla said:

    @Maciejasjmj said:
    Worse. Say you have Arr = [1, "John", 0.5]["John", "3", "1516.0"].

    FTFY

    If you were aiming for the joke I think you were aiming for, you were @OffByOne.



  • :pout.zip:


  • BINNED

    Yes this is one of those quirks, but fortunately PyCharm gives you this nice wiggly red underlines to mark that as a warning about shadowing a.



  • @Yamikuronue said:

    compile-time [...] generation of code

    This is the same problem I have with Rails: I can't debug what I can't see.


    Still, macros are a way better way to do this than monkeypatching. Especially if there's a way to view the transformed source.

    type-checked but without having to specify the type

    What if it guesses wrong?

    That's when you need to annotate. The point is that 99% percent of the time it can guess right, so that's what should be optimized for.

    We want the compiler to understand what we mean without having to specify

    You want magic.

    If anything, they don't want enough magic. Local inference is kinda meh compared to how rust does it.

    An Array can have mixed types, meaning T will be a union of types, but these are determined when the array is created

    In what situation is this useful? It sounds like a fucking mess.

    Agreed.

    name, age = "Crystal", 1

    The above is the same as this:

    temp1 = "Crystal"
    temp2 = 1
    name = temp1
    age = temp2

    This sounds like unnecessary overhead

    Looks like bog-standard destructuring to me. A new language coming out in this decade without stuff like that would be trwtf.

    Tbh, this looks like a language that I could enjoy programming in. Working with gradle has gotten me inured to method calls without parentheses, and my biggest gripe with ruby — mutable strings — crystal doesn't have. But I don't want or need a new language. I guess the only thing I'm really excited about here is the thought of bringing some static-typing culture to those ruby hooligans.



  • @Buddy said:

    my biggest gripe with ruby — mutable strings
    My biggest gripe with Java is immutable strings. Languages like Rust and C++ let you choose on an alias-by-alias basis.



  • Sure, more options is always better. But if I had to choose between a language without mutable strings and one without immutable, no contest.



  • @LB_ said:

    Languages like Rust and C++ let you choose on an alias-by-alias basis.

    All C++ std::strings are mutable. All C++ string literals are immutable. That's hardly a choice.



  • std::string is mutable
    std::string const is immutable
    How isn't that a choice?

    As for string literals, just put them in a std::string if you want to mutate them...



  • @LB_ said:

    ```plain
    [15:29] <jhass> LB__: IME if you really have the problem of forgetting which local variables are valid in your current scope, you write way too long methods

    
    The simple fact that the developer seems to have no problem in quickly stating that 'you are to blame' as an explanation of, and justification for, the poor behaviour of their product is reason enough to look elsewhere.
    
    Filed under: [Doing It Wrong™](), [Discourse, anyone?]()

  • Discourse touched me in a no-no place

    @LB_ said:

    My biggest gripe with Java is immutable strings.

    The good thing about immutable strings is that you don't need to worry about the rug being pulled out from under your feet; that sort of bug is horrible to hunt down. If you need a mutable, you just use a StringBuilder and you know not to share it around like an idiot.


  • Discourse touched me in a no-no place

    @LB_ said:

    @jhass said:
    you write way too long methods

    Atomising your code into large numbers of very short methods doesn't help that much, as the spaghetti moves into the call pattern. IMO the real solution is to be much clearer about what each class and method does and its relation to the overall whole; once you've got that, you don't need to worry so much about the lengths of individual methods as they'll have strong enough coherence in the first place.



  • @LB_ said:

    std::string const is immutable

    ...without any benefits of immutable strings.


  • BINNED

    @dkf said:

    you just use a StringBuilder and you know not to share it around like an idiot.

    Qt's QStrings are mutable but COW and you can share them as you like.



  • What benefit would a immutable string have that const std::string doesn't? I don't use Java so I don't know how it helps there.



  • Visual Basic can have Option Explicit. Maybe you can send the link to them for reconsideration.



  • @Kian said:

    What benefit would a immutable string have that const std::string doesn't? I don't use Java so I don't know how it helps there.

    Same as any immutable structure - cheap copy, memory efficiency, and guarantee no one will change the content of the string without the owner's consent.

    Although I found a link that suggests Java resigned from one of these.



  • @Gaska said:

    cheap copy
    What about immutable objects makes them cheap to copy? Why would you even want to copy an immutable object?@Gaska said:
    memory efficiency
    Um, what? The way Java does it makes it look like the least memory efficient thing in the world.@Gaska said:
    guarantee no one will change the content of the string without the owner's consent
    You used the word "owner" here, so does that mean that you're okay with the owner changing the content even if other places have access through const references? Because this is the only thing that is generally considered a problem with const in C++ and is the only advantage of immutable objects that I know of.



  • @LB_ said:

    What about immutable objects makes them cheap to copy?

    COW

    @LB_ said:

    Why would you even want to copy an immutable object?

    I meant logical copy (a separate object in identical state), not physical copy (a separate block of memory with identical content).

    @LB_ said:

    Um, what? The way Java does it makes it look like the least memory efficient thing in the world.

    Yep. That's Java for you. Doesn't mean all immutable strings are the same. C# does a much better job here IMO.

    @LB_ said:

    You used the word "owner" here, so does that mean that you're okay with the owner changing the content even if other places have access through const references?

    No, I'm not. Neither am I OK with casting immutable reference into mutable. The problem with C++ is that it allows both.

    @LB_ said:

    Because this is the only thing that is generally considered a problem with const in C++

    There's also the "symbol resolution with cv-qualifiers and type promotions" shitfest. Especially fun if template argument deductions are involved too.



  • All this implementation stuff is great, really, but I think my beef with mutable strings is more of a style/usability/design thing. For 99% of string handling, I am willing to eat any performance costs just to not have to worry about strings changing out from under me or having to do defensive copies of strings or whatever. So that's what I want from language primitives, and for the rest of use cases, I'd rather use a library that's specifically designed for what I'm doing, rather than just the jack-of-all-trades builtins.

    Mutable strings kind of make sense in c(++), but in a high-level language, there's no excuse. Strings just aren't an appropriate type of object for sharing state through.



  • @Gaska said:

    COW

    Any idea why libstdc++ switched from COW to SSO, or why libc++ has been using SSO from the start? (I actually don't know).

    @Gaska said:

    I meant logical copy (a separate object in identical state), not physical copy (a separate block of memory with identical content).
    We must have different definition of the word 'object' because, to me, an object's identity is its address in memory.

    @Gaska said:

    The problem with C++ is that it allows both.
    Doesn't C# have unsafe blocks? I'm not sure why this is an issue - a const_cast stands out in code pretty well and when you see it you know someone is doing something seriously wrong. As for const not being a true immutable, I tend to agree, but in languages that only have immutable and mutable and no const correctness, I feel naked.


    I still don't understand what COW or copying in general has to do with immutable objects. I'm not sure why you'd ever want an exact duplicate of an immutable object.


    @Buddy said:

    have to worry about strings changing out from under me or having to do defensive copies of strings or whatever
    I've never really had that happen

    @Buddy said:

    Mutable strings kind of make sense in c(++)
    Because C and C++ have a language feature that is not present in nearly every other language: const correctness. I feel naked without it, so I can see why people who primarily work in other languages want immutable types.



  • @LB_ said:

    Any idea why libstdc++ switched from COW to SSO, or why libc++ has been using SSO from the start? (I actually don't know).

    IIRC the COW versions were of dubious legality even pre-C++11, because changing the string via operator[] would invalidate references to individual elements and (some*) iterators.

    N2534 additionally mentions some issues with concurrency.

    (*) Calling begin() to get a non-const_iterator already unshares the string in libstdc++, so those iterators aren't invalidated.


  • Discourse touched me in a no-no place

    @LB_ said:

    I still don't understand what COW or copying in general has to do with immutable objects. I'm not sure why you'd ever want an exact duplicate of an immutable object.

    The model of a good string API (that doesn't cause spooky-action-at-a-distance headaches) is that operations that cause a change to the string actually return a new string that is equal to the old string with the change applied to it. In implementation terms, this sucks because it's inclined to be dog-slow. However, if you can determine that the string only has one handle to it, you can do the modification in-place and return the same object; that's much faster. This sort of thing leads naturally to the copy-on-write approach. Determining if you only have one reference to the string is non-trivial, especially in the presence of threads. In fact, working out how to do it with threads is likely to give me a headache.

    True immutable strings don't have that sort of problem. They're memory-expensive, but work better in a multi-threaded environment. (That's Java for you.)



  • @LB_ said:

    Any idea why libstdc++ switched from COW to SSO, or why libc++ has been using SSO from the start? (I actually don't know).

    Lawyerish bikeshedding in ISO C++11. Some very obscure part of std::string API made COW impossible.

    @LB_ said:

    We must have different definition of the word 'object' because, to me, an object's identity is its address in memory.

    We do. For me, an object is a set of data and associated functions that perform operations on this data. The fact that object occupies memory is implementation detail.

    @LB_ said:

    Doesn't C# have unsafe blocks?

    I don't know; never heard nor used them. So instead, I'll talk about Rust unsafe blocks. For me, they're perfectly fine - the language spec says that inside unsafe block, there are no guarantees about anything so the programmer can do what the fuck they want, but after unsafe block ends, the programmer is obliged to restore all invariants he has violated in process. Including that all immutably borrowed objects must be in the same state as they were before. If some invariant is violated and not restored, it is an ill-formed code per Rust spec. And it's okay to assume ill-formed code doesn't exist (until proven otherwise).

    @LB_ said:

    I'm not sure why this is an issue - a const_cast stands out in code pretty well and when you see it you know someone is doing something seriously wrong.

    The difference is, const_cast is well-formed code, so technically speaking, it's okay to do this, so you can't assume no one does this. Of course, in practice no one does this, but still.

    @LB_ said:

    Because C and C++ have a language feature that is not present in nearly every other language: const correctness.

    Don't make me laugh. C barely has any "correctness" at all, and C++ makes handling const such a PITA that almost no one does this all the way through, and if it's not done all the way through, it's almost useless.



  • @dkf thanks! That helps a lot. I think concurrency issues are my main concern though - I'd rather that generic classes like strings should work in all cases, not be optimized for "more common" cases.

    @Gaska said:

    The fact that object occupies memory is implementation detail.
    Yes, but the fact that you can address an object isn't. The object might be on disk and not in RAM but that doesn't mean I can't address it. I'm totally confused by what you think an object is.

    @Gaska said:

    C++ makes handling const such a PITA that almost no one does this all the way through
    I keep seeing this complaint come up but I've never actually experienced this issue myself. Yes, having two overloads with the same code and one is const while the other isn't is a pretty lame aspect of the way const correctness is implemented in C++, but the benefits for client code are numerous - it basically lets the user choose on a per-alias basis whether they want to allow an object to be modified through that alias without having two disparate classes. I'm pretty sure Rust does something similar, doesn't it?

    @Gaska said:

    if it's not done all the way through, it's almost useless.
    Which is exactly why nobody uses const_cast - it's basically equivalent to goto except that goto actually has one valid use in C++.


    I try to be open-minded, but Rust is the only other language where I've seen anything even resembling a usable type system. In other languages you either have to resort to mostly immutable types or try to manually enforce type safety that the compiler should be enforcing. Isn't the point of a type system to be flexible and safe? Maybe I'm blinded by const-correctness, though - if you believe that's the case, please enlighten me! I'm willing to listen.



  • @LB_ said:

    Yes, but the fact that you can address an object isn't.

    Depends on what you mean by addressing.

    @LB_ said:

    The object might be on disk and not in RAM but that doesn't mean I can't address it.

    FWIW, the object might be in another universe, might come from the future, or lack a memory representation at all - and you should still be able to address it.

    @LB_ said:

    Yes, having two overloads with the same code and one is const while the other isn't is a pretty lame aspect of the way const correctness is implemented in C++, but the benefits for client code are numerous

    Too bad the developers of many libraries don't think the latter is worth the former - the result is, they don't declare their methods as const, and I can't use their code in const-correct way even if I wanted - and I can still get my work done just fine, which further reinforces their point.

    @LB_ said:

    I'm pretty sure Rust does something similar, doesn't it?

    In Rust, constness isn't a property of a type, but rather of a variable and of a reference. A small difference for a typical user, but makes a world of difference when you get into details - for instance, no code duplication is necessary anymore (except the bare minimum you had to do anyway, like having both iter() and iter_mut() methods).

    @LB_ said:

    I try to be open-minded, but Rust is the only other language where I've seen anything even resembling a usable type system.

    That's because all the other languages sacrifice compile-time features for better run-time features.



  • @Gaska said:

    Yep. That's Java for you. Doesn't mean all immutable strings are the same. C# does a much better job here IMO.

    Eeeeh... I am not so sure about that. Have experienced plenty of runaway memory use due to strings in C#. Granted our product is a child of 10 years of ever expanding requirements, quick hacks and no time to fix deep issues (because they would take to long and not fit into our release schedule (which anyways always slips by upwards of 100% due to previously mentioned deep issues)).



  • @HardwareGeek said:

    you were @OffByOne.

    So if @boomzilla is me and he is Blakeyrat, does that mean I'm Blakeyrat too?


  • area_deu

    I'm sure you're @Kuro.



  • @OffByOne said:

    So if @boomzilla is me and he is Blakeyrat, does that mean I'm Blakeyrat too?

    I'm sure there's @accalia here somewhere...


  • sockdevs

    @Gaska said:

    @OffByOne said:
    So if @boomzilla is me and he is Blakeyrat, does that mean I'm Blakeyrat too?

    I'm sure there's @accalia here somewhere...

    /me waves.

    yes, but she's only here because she's lost and can't find the exit door at the moment.



  • @Gaska said:

    @OffByOne said:
    So if @boomzilla is me and he is Blakeyrat, does that mean I'm Blakeyrat too?

    I'm sure there's @accalia here somewhere...

    No, but my gramming was probably less than perfect. "he" refers to @boomzilla.



  • No, I didn't mean it like that.



  • Then I'm wooshing on what you mean.



  • @Gaska said:

    Same as any immutable structure - cheap copy, memory efficiency, and guarantee no one will change the content of the string without the owner's consent.
    Cheap copy and memory efficiency (what COW gives you) is a trade off, the copy is cheap but management becomes more complex. Sure, if you have already paid for GC, then you might as well optimize for it. But it's not something that is inherently better.

    It doesn't even have anything to do with immutability. You could have COW for mutable objects. They could start as a pointer to a ref-counted structure, and whoever makes a change when there's more than one ref makes a copy. It's just lazy copying.

    As for anyone changing immutable objects, const_cast doesn't make it legal to mutate a const object. It's there just as a help for people that have to work with codebases that aren't const-correct. You are still not allowed to mutate an object that is const, or through a const reference or pointer to const. It simply makes it possible to call functions that don't promise not to mutate it. If they do mutate it, you enter UB land. So you have the same guarantee that no one will mutate it without permission (or at all, if the object itself is declared const).

    @Gaska said:

    There's also the "symbol resolution with cv-qualifiers and type promotions" shitfest.
    What shit-fest? It's really not that hard. If you promise not to change something, then don't change it. Do not call things that don't make the same promise, since they might change it and break your promise.

    The rules can get complex, but that's intended to make it straight-forward to use. For instance, if you make a copy of something const, you probably don't want your copy to be const too. Rather than add syntax to say "I'm stripping the const on my copy", copies strip the cv-qualifier by default. So if you want a const copy, which is the rare case, you have to specify it.

    @Gaska said:

    We do. For me, an object is a set of data and associated functions that perform operations on this data. The fact that object occupies memory is implementation detail.
    Well, this is an issue of language I suppose. In C++, an object is defined by the space it occupies in memory. An object's lifetime starts when memory is allocated for it and any non-trivial constructor finishes, and ends when it's non-trivial destructor finishes or memory is released. During it's lifetime, the object is the memory it occupies.

    Keep in mind that a pointer is an object separate from the object it points to. So if you have a non-null pointer, you actually have two objects. Same if you have a larger structure with pointers or references. The stuff those pointers or references point to are not part of the object that holds the references or pointers, from a language standpoint. They may manage that memory, so that logically they are one single abstraction, but in language terms they are different objects with different lifetimes.

    @Gaska said:

    The difference is, const_cast is well-formed code, so technically speaking, it's okay to do this, so you can't assume no one does this. Of course, in practice no one does this, but still.
    As I said above, const_cast is there just to help you call code that isn't const correct. Removing const so you can modify something is wrong. "Wrong but compiles" always means UB. If you need to modify something, don't make it const in the first place.

    So go ahead and assume no one will do it. It won't lead to a more broken program than you already have if someone breaks that promise.

    @Gaska said:

    C++ makes handling const such a PITA that almost no one does this all the way through, and if it's not done all the way through, it's almost useless.

    If you are having problems with const, that's the compiler trying to help you write better code. Tell your library vendors to fix their shit, or move to a library that doesn't have that problem. Or use const_cast, which helpfully documents all the places where the library is broken and in need of repair.

    @Gaska said:

    In Rust, constness isn't a property of a type, but rather of a variable and of a reference.
    That's literally how const works in C++. How is having a const overload for certain access methods more annoying than having an iter and iter_mut method?

    I'd say the overloaded approach is better, in fact, because it makes it possible to write more flexible generic code. And even when not writing templates, it allows code that does the same thing to look the same.



  • @LB_ said:

    Which is exactly why nobody uses const_cast - it's basically equivalent to goto except that goto actually has one valid use in C++.

    IMHO there's one valid use for const_cast, too, and it is precisely to avoid repeating long member function bodies for const and non-const cases:

      return_type const& get_thingie() const;
      return_type& get_thingie() { return const_cast<return_type&>(static_cast<my_class const&>(*this).get_thingie()); }
    

    It's ugly as hell, yes, but it gets the job done, and is in my opinion preferable to the only alternatives of a) repeating the whole body of get_thingie() or b) discarding const correctness.

    I have to admit I often feel naked, too, in languages which don't have "const correctness".


  • Discourse touched me in a no-no place

    @LB_ said:

    I'd rather that generic classes like strings should work in all cases, not be optimized for "more common" cases.

    Well, COW works wonderfully for strings in the still very common single-threaded cases. It's a part of why there's quite a few languages that are better at string handling code than C++ is. COW only really falls apart for multi-threaded code, and that's when it crumbles in a mess (because the cost of deciding whether you're using a shared reference or not becomes very high, which has a lot of consequential impacts). However, most programmers aren't good a thinking in terms of multi-threaded code in the first place, so it's not as big a restriction as all that.

    Handling “all cases” is frequently harder than it appears to be at first glance. Sometimes it is easier to restrict the space of “all cases” so that it excludes the gnarliest options than it is to get that last 1–2% of tricksiness (particularly as that can have a lot of impacts downstream).


Log in to reply
 

Looks like your connection to What the Daily WTF? was lost, please wait while we try to reconnect.