Shoggoth
kiwifarms.net
- Joined
- Aug 9, 2019
Follow along with the video below to see how to install our site as a web app on your home screen.
Note: This feature may not be available in some browsers.
This reminds me of something Joe Armstrong has said. Don't design your system for 10 users and scale it up. Design it for 10M users then scale it down.
I don't quite get what you're going for here. Both of those cases would be relatively simple to implement in a contemporary OOP-ish language where you can create custom classes/types. For the segment type, have its constructor take x0 and x1 as parameters and throw an exception if x0 is larger than x1 (or swap their values if that is acceptable). For the latter, have the collection type's "addSegment()" method throw an exception if the x0 of a new segment is smaller than the x1 of the previous segment (or just have "getAllSegments()" sort the segments when called, again depending on what is acceptable behavior). I don't see what this has to do with static typing, either.One of the problems with typed languages is that besides ossifying the code base over time, they don't really solve the problem, not without plenty of case and nice features, of making illegal states impossible to represent.
Example: I have type representing a segment. x1 should always be > x0.
I have a collection of segments which should always be in rising order.
Can your type system represent this? Can it generate example data for this?
Is it an optimization? The scaled down system might perform worse than one designed for 10 users. The idea is that some tools make designing for scalaility (almost) trivial, such as Erlang, as Armstrong will gladly tell you.Nice quote, but I'd argue it's not really necessary all the time. Premature optimization and letting perfect be the enemy of good are real problems too.
That's exactly the point of a good/strong type system.I don't quite get what you're going for here. Both of those cases would be relatively simple to implement in a contemporary OOP-ish language where you can create custom classes/types. For the segment type, have its constructor take x0 and x1 as parameters and throw an exception if x0 is larger than x1 (or swap their values if that is acceptable). For the latter, have the collection type's "addSegment()" method throw an exception if the x0 of a new segment is smaller than the x1 of the previous segment (or just have "getAllSegments()" sort the segments when called, again depending on what is acceptable behavior). I don't see what this has to do with static typing, either.
Segment :: {x0 :: double, x1 :: double}
Better
Segment :: {x0 :: double, x1 :: double}, x1 > x0
Stronger if you can also specify an ordering semantic
Segments :: List[Segment] // Meh
Segments :: List[Segment], Monotonic increasing // Yeah
Hah, I was just thinking about that Knuth quote.Nice quote, but I'd argue it's not really necessary all the time. Premature optimization and letting perfect be the enemy of good are real problems too.
I don't think the point of the saying is that you shouldn't try to make things efficient, but that iteration will usually give you better results than trying to presuppose your future needs and structuring everything under that assumption from the getgo.Is it an optimization? The scaled down system might perform worse than one designed for 10 users. The idea is that some tools make designing for scalaility (almost) trivial, such as Erlang, as Armstrong will gladly tell you.
You could argue that a good language, environment and tooling will support it from the get-go. Will the extra 5-10% of work early on be worth it down the line? Unless you're rushing to market, why not?Hah, I was just thinking about that Knuth quote.
I don't think the point of the saying is that you shouldn't try to make things efficient, but that iteration will usually give you better results than trying to presuppose your future needs and structuring everything under that assumption from the getgo.
Why?Show me a good type system
I don't want a runtime exception. I want illegal state to be impossible in my system.
So are you basically saying that you want this "legality checking" to be a function of the language itself rather than expressed in the language? If so, I can see that, but if you have rather complex requirements for what is "legal," I think that could get awkward.Code:Segment :: {x0 :: double, x1 :: double} Better Segment :: {x0 :: double, x1 :: double}, x1 > x0 Stronger if you can also specify an ordering semantic Segments :: List[Segment] // Meh Segments :: List[Segment], Monotonic increasing // Yeah
Not only complex, but it's more trouble than it's worth.So are you basically saying that you want this "legality checking" to be a function of the language itself rather than expressed in the language? If so, I can see that, but if you have rather complex requirements for what is "legal," I think that could get awkward.
I've had some pretty complex requirements in the past. If you're careful, it doesn't get hairy, and you don't have to deal with a zoo of inheritance and iterfacesIt could be made impossible in the examples I gave. If an exception is thrown when you pass an x1 smaller than x0, the segment was not created and is thus not illegal. Same with the collection example if you do checking in the "addSegment()" method and throw an exception from there if it's wrong. What would you rather have instead? The program halts (which is what would happen if the exception isn't caught anyway)?
So are you basically saying that you want this "legality checking" to be a function of the language itself rather than expressed in the language? If so, I can see that, but if you have rather complex requirements for what is "legal," I think that could get awkward.
What will happen is the same thing that happens when you try to put a string where an int should go. It's an exceptional situation and should be represented as such. It will throw, probably. The difference is that relations and types are way better than your badly hand crafted ad-hoc validation.Not only complex, but it's more trouble than it's worth.
What you'll see in practice is that more and more logic would be poured into such checks and this solves fuck-all, merely the complexity would be transfered from imperative statements into a more functional or declarative approach. Which is precisely what this idea is all about - make the world Functional™.
And what should happen if the checks are being violated, for example when reading input from user, database, file or malicious actor? Crash and burn? Exception thrown? How are errors handled? You can't handwave the issue away by saying "type system doesn't allow for invalid segments to exist", unless you're planning on either not taking any input at all or handling it in some "unsafe" interface block/module boundary, in which case - once again - the complexity does not disappear, is just transferred.
It's all so tiresome.
This is just more handwaving, "way better" - says who? Why not the other way around - hand crafted validation is way better than badly constructed ad-hoc relations? Two can play that game, because it's the same complexity, but transferred to a different semantic construct. It's broadly the same as making a checked setter in a OOP fashion, just as @Least Concern stated. You get the same guarantees as you would in this hypothetical Prologish type system.What will happen is the same thing that happens when you try to put a string where an int should go. It's an exceptional situation and should be represented as such. It will throw, probably. The difference is that relations and types are way better than your badly hand crafted ad-hoc validation.
It matters because you can have guarantees regarding data percolating inside the system. It lets you generate data and test your assumptions.
If you're tired, go back to sleep.
"Any sufficiently advanced type systems contains a slow bug ridden partial implementation of Prolog"This is just more handwaving, "way better" - says who? Why not the other way around - hand crafted validation is way better than badly constructed ad-hoc relations? Two can play that game, because it's the same complexity, but transferred to a different semantic construct. It's broadly the same as making a checked setter in a OOP fashion, just as @Least Concern stated. You get the same guarantees as you would in this hypothetical Prologish type system.
I'm not entirely sure on what do we actually agree or disagree about. And I mean that in an honest, good faith manner.Do we disagree on ad-hoc imperative validation being worse than the concentrated efforts of library or language maintainer(s) on implementing an expressive and flexible validation framework?
assert()
or if () throw();
statement to enforce the guarantee. Nice quote, but I'd argue it's not really necessary all the time. Premature optimization and letting perfect be the enemy of good are real problems too.
I don't want to get too involved in this clash of autism, but the sentiment of "don't design your system for 10 users and scale it up; design it for 10M users then scale it down" is hardly a "premature optimization" when rebuilding an established web service that is constantly growing and subject to DDoS by hostile parties. I believe the full quote from Knuth is:Hah, I was just thinking about that Knuth quote.
--------------The real problem is that programmers have spent far too much time worrying about efficiency in the wrong places and at the wrong times; premature optimization is the root of all evil (or at least most of it) in programming.”
Python is "slow" in the sense that it is fucking slow.Python is "slow" in the sense that if you were to compare
I don't want to get too involved in this clash of autism, but the sentiment of "don't design your system for 10 users and scale it up; design it for 10M users then scale it down" is hardly a "premature optimization" when rebuilding an established web service that is constantly growing and subject to DDoS by hostile parties
Making a forum run smoothly for 5k concurrent users is a very different load than making one run smoothly for 5k concurrent users while it's also under a significantly large DDoS attack.Choosing your language/framework based off of some hypothetical sky-high user count IS premature optimization. It's currently running off of PHP. There's an average of like 5k concurrent users. It's a forum. You don't NEED anything in particular to make that work effectively
tbh I have no idea why we were having a fight either. Pax.I'm not entirely sure on what do we actually agree or disagree about. And I mean that in an honest, good faith manner.
I think you're speaking from a position of a person who had to deal with very specific types of data and validity of thereof, in very specific types of problems, where you had to fight with validity issues of this data constantly, on many layers of the software system. This is the impression I'm getting, correct me if I'm wrong.
I'm not seeing the same issues in the same light as you do, maybe because I wasn't touched as hard in a no-no place by such problems (and therefore not seeing some particular, potential bugs) or maybe I consider OOP approach as sufficiently strong to enforce the same guarantees. And yes, those guarantees can and will propagate throughout the system - how they cannot, if they're baked into the objects themselves?
Consider the segment example, where you modify the x0 or x1 coordinates using a setter function. Provided that you perform all modifications throughout the setter, that's literally the one and only place where you need to place anassert()
orif () throw();
statement to enforce the guarantee.
Is it more cumbersome than marking a x0 < x1 relation in the type system? Sure! Is it more vulnerable to coding errors? Possibly, the programmer has to remember that every x0 or x1 modification has to be performed throught the setter method or else the guarantee doesn't hold anymore. So is it worse than a relation in type system? No. Is it better? No.
OK, so how is it not worse even though I myself am admitting that there are vulnerabilities? Because of the following caveats, in no particular order of importance:
- What if there are logic errors in the relations? How does that differ from making a coding mistake in a method?
- How would one check if the relations are satisfied and non-contradictory when some compounding is added to the types? Would such checked types be simply verboten from being compounded in more complex types? Think: I have a Segment type, now I want a Polygon type being a collection of Segments. Should each Segment in a Polygon obey the x0 < x1 relation? That's not very convenient. Should programmer define another Segment type which does not enforce the relation? Then it's even worse because you have another similar type and you might use the "incorrect" Segment type in some place.
- Validity checking is a lot more than just simple relations on numeric values. Back to the Polygon example: I want to ensure that the points are ordered CCW. How would such complex checks be handled? Either the type system must evolve to contain a Turing-complete language (and then you get C++) or be constrained to almost trivial cases and types in which its usefulness gets immediately exhausted and back to imperative algorithms you go.
- A bit of a goalpost moving: what about the performance hit (branches, branches everywhere) of doing such complex checks at each and every object modification (and maybe also access).
The fact that most static type systems don't have dependent types makes them almost useless for detecting bugs in anything but purely scalar code.
In very rough pseudo code:
Code:Segment :: {x0 :: double, x1 :: double} Better Segment :: {x0 :: double, x1 :: double}, x1 > x0
Segment
with a public constructor that swaps the incoming values as needed to enforce the "x0 ≤ x1" invariant. Java-ish pseudo code:class Segment {
private double from;
private double to;
public Segment(double from, double to)
{
if (from <= to)
{
this.from = from;
this.to = to;
}
else
{
this.from = to;
this.to = from;
}
}
// ...
}
Code:Stronger if you can also specify an ordering semantic Segments :: List[Segment] // Meh Segments :: List[Segment], Monotonic increasing // Yeah
MonotonicList<Segment>
, whose public interface does not provide a way to insert an element in an arbitrary position - instead, its public insert
method would always automatically insert the element in the correct position. The "monotonic increasing" invariant is thus ensured, and again without the need for run-time error handling.The big sell of having this constraints as part of the type system (you'll want to compile them into the constructors like you've hand coded, too) is constraint propagation. You only know to > from at construct time. Imagine this knowledge went with the object's type everywhere you used it in the system. You could infer pretty complex and useful stuff regarding data floating around in your system.Hello fellow kiwis who have Learned to Code™, allow me join the 'sperging.
I strongly disagree with the "almost useless" part.
When I compare my experiences with Python on the one hand and C++/Java on the other, the static typing in the latter has been very helpful in detecting bugs at compile time that could have easily slipped into the finished software when using Python.
Sure, the static type system can't prevent all buggy code, but it can prevent a lot of it.
You'd implement this as a custom typeSegment
with a public constructor that swaps the incoming values as needed to enforce the "x0 ≤ x1" invariant. Java-ish pseudo code:
Code:class Segment { private double from; private double to; public Segment(double from, double to) { if (from <= to) { this.from = from; this.to = to; } else { this.from = to; this.to = from; } } // ... }
This way, users of your type cannot accidentally construct an object of it that does not satisfy the desired invariant. No need for exceptions or other run-time error handling.
Same deal: You'd define a custom type, sayMonotonicList<Segment>
, whose public interface does not provide a way to insert an element in an arbitrary position - instead, its publicinsert
method would always automatically insert the element in the correct position. The "monotonic increasing" invariant is thus ensured, and again without the need for run-time error handling.
Is there a real benefit to defining such invariants in the type system itself (as you endorse) rather than in the implementation of individual custom types (as I've shown above)?
I'm not a CompSci academic, so it's quite possible I'm missing something here.
I disagree that the main driver for language choice should be ease of transitioning from the previous language: the driver should be the benefits gained from transitioning weighted against the cost of the transition. I fail to see any benefits in Python given this scenario.Choosing your language/framework based off of some hypothetical sky-high user count IS premature optimization. It's currently running off of PHP. There's an average of like 5k concurrent users. It's a forum. You don't NEED anything in particular to make that work effectively, and the main drivers for choice IMO should be ease of transitioning from PHP, support for any modules that might need to be replaced from losing Xenforo, and breadth of tutorials or stack overflow answers since he'll be going into anything other than PHP relatively blind.