[HN Gopher] Why static languages suffer from complexity
___________________________________________________________________
Why static languages suffer from complexity
Author : Lapz
Score : 117 points
Date : 2022-01-19 16:55 UTC (6 hours ago)
(HTM) web link (hirrolot.github.io)
(TXT) w3m dump (hirrolot.github.io)
| Animats wrote:
| Fascination with type systems does not seem to be all that useful
| in practice. Go has a minimal type system, and is able to do much
| of Google's internal server side work.
|
| Most of the problems that cause non-trivial bugs come from
| invariant violations. At point A, there's some assumption, and
| way over there at point B, that assumption is violated. That's an
| invariant violation.
|
| Type systems prevent some invariant violations. Because that
| works, there are ongoing attempts to extend type systems to
| prevent still more invariant violations. That creates another
| layer of confusing abstraction. Some invariants are not well
| represented as types, and trying makes for a bad fit. What you're
| really trying to do is to substitute manual specification of
| attributes for global analysis.
|
| The Rust borrow checker is an invariant enforcer. It explicitly
| does automatic global analysis, and reports explicitly that
| what's going on at point B is inconsistent with what point A
| needs. This is real progress in programming language design, and
| is Rust's main contribution.
|
| That's the direction to go. Other things might be dealt with by
| global analysis, Deadlock detection is a good example. If P is
| locked before Q on one path, P must be locked before Q on all
| paths. There must be no path that leads to P being locked twice.
| That sort of thing. Rust has a related problem with borrows of
| reference counted items, which are checked at run time and work a
| lot like locks. Those potentially have a double-borrow problem
| related to program flow. I've heard that someone is working on
| that for Rust.
| DonaldPShimoda wrote:
| > Fascination with type systems does not seem to be all that
| useful in practice.
|
| > ...
|
| > The Rust borrow checker is an invariant enforcer. [...] This
| is real progress in programming language design, and is Rust's
| main contribution.
|
| I'm so confused by your stance here. You essentially say "type
| systems are not useful" and then "oh but this most recent
| advance in type systems -- that one is useful." Do you find
| type systems useful or not?
|
| There are a lot of properties we can analyze statically, and
| practically all of them essentially amount to extensions of
| type systems. Any of them increases our ability to rule out
| undesirable programs from every beginning execution. Some of
| them have unintuitive syntax, but many of them are no more
| syntactically burdensome than most other type systems. This is
| especially true if you consider how far we've come with type
| inference, so we no longer have to write code with the
| verbosity of Java just to get some meager guarantees. It's
| still a very active area of research, but we're clearly making
| progress in useful ways (which you even highlight), so I don't
| really know what point it is you've set out to make.
| joe_the_user wrote:
| _I 'm so confused by your stance here. You essentially say
| "type systems are not useful" and then "oh but this most
| recent advance in type systems -- that one is useful." Do you
| find type systems useful or not?_
|
| Not the original author but it seems like they're saying that
| type-systems are non-specific invariant enforcers and so have
| costs without necessarily having benefits whereas a user-
| specifiable invariant enforcer is more guaranteed to have the
| benefits.
| preseinger wrote:
| Type systems are useful, but not nearly as useful as many
| people believe they are.
| mountainriver wrote:
| Try working in a big system without them =P I think they
| are invaluable
| Verdex wrote:
| I think the key is the following:
|
| > It explicitly does automatic global analysis
|
| They appear to think that the borrow checker isn't achieved
| with type theory, but with some other technique ("global
| analysis").
|
| Although, to be fair, my understanding of making a practical
| affine type checker is that things get kind of wonky if you
| do it purely logically. So practically you do some data flow
| analysis. Which is, I believe, what rust is doing. This also
| explains why MIR was such a big deal for certain issues with
| borrow checking. They ended up with a format that was easier
| to run a data flow analysis on, and that allowed the borrow
| checker to handle things like non-lexical lifetimes, etc.
|
| [I've only read about such things. So I might have mis-
| remembered some of the details. However, this is my take on
| why someone might not call rust's advances purely type
| theoretic (even if they can be handwaved as type theory at a
| high level).]
| Zababa wrote:
| > Go has a minimal type system, and is able to do much of
| Google's internal server side work.
|
| Isn't it stil mostly Java and C++? That's what I hear all the
| time here.
|
| Also, I'm not sure what point you're trying to make. You start
| by saying that fascination with types systems is not useful in
| practice, and end with an example where it is useful (Rust).
| While Go can stick a GC to avoid most of the issues that Rust
| is trying to solve, it stil has to ship with a defer mechanism
| (no linear/affine types/RAII) and a data race detector.
| acchow wrote:
| > Isn't it stil mostly Java and C++? That's what I hear all
| the time here.
|
| Go's type system is much weaker and less expressive than
| either Java's or C++'s. C++ in particular has parametric
| polymorphism, type constructors, and dependent types. Go has
| none of those.
| skybrian wrote:
| It's been a while since I worked there, but the trend at
| Google at the time was that the amount of code written in
| each popular language was rapidly growing, and the number of
| popular languages was also slowly growing. (Despite a lot of
| resistance to introducing new languages.)
|
| I'm out of touch, but I would expect that there is a lot more
| Go code by now, and it also didn't catch up with C++ or Java.
| Chyzwar wrote:
| Rust contribution affect less than 1% of programmers. Most code
| written today do not require manual memory management or even
| explicit multithreading.
|
| I think typescript with gradual and structural typing and
| similar like mypy or sorbet are making real difference.
|
| Type systems provide multiple benefits, performance, self-
| documentation, better tooling and more explicit data model.
| agentultra wrote:
| > Fascination with type systems does not seem to be all that
| useful in practice.
|
| And yet type theory is an excellent way to express all kinds of
| invariants. The more rich the type system the more you can
| express. If you get to dependent types you essentially have all
| of mathematics at your disposal. This is the basis of some of
| the most advance proof automation available.
|
| What is super cool is that proofs are programs. You can write
| your programs and use the same language to prove theorems about
| them.
|
| This is still fairly advanced stuff for most programming
| activities but the languages have been steadily getting better
| and the automation faster and I think the worlds will
| eventually collide in some area of industrial programming.
| We're already seeing it happen in security, privacy, and
| networking.
|
| I don't think type systems suffer from complexity. They merely
| reveal the inherent complexity. You can use languages that hide
| it from you but you pay a price: errors only become obvious at
| run time when the program fails. For small programs that's easy
| enough to tolerate but for large ones? Ones where certain
| properties cannot fail? Not so much in my experience.
|
| _update_ : clarified wording of "proofs are programs"
| dmitriid wrote:
| > The more rich the type system the more you can express. If
| you get to
|
| Ah yes. And then you end up writing entire prgrams in types.
| So the next logical setep would be to start unit- and
| integration tests for these types, and then invent types for
| those types to more easily check them...
|
| > you essentially have all of mathematics at your disposal.
|
| Most of the stuff we do has nothing to do with mathematics.
| spc476 wrote:
| > You can write your programs and use the same language to
| prove theorems about them.
|
| Didn't Kurt Godel and Alan Turing do some work on proving
| statements within a system?
| BalinKing wrote:
| AFAIK languages like Idris, Agda, and Coq are not Turing-
| complete (specifically, they disallow general recursion)
| for just this reason.
| raphlinus wrote:
| If my understanding is correct, Agda and Coq disallow
| general recursion, so all programs terminate by
| construction, but Idris relaxes this, in a desire to be
| more pragmatic at the cost of not as clean mathematically
| (functions have to be modeled as partial in general, to
| account for the fact they might not terminate).
| acchow wrote:
| > Go has a minimal type system, and is able to do much of
| Google's internal server side work.
|
| And yet Go is adding generics in 1.8. And I'm sure its type
| system in another 5 years will be much more expressive than
| 1.8's. The community has long been saying that the minimal type
| system isn't enough.
| benhoyt wrote:
| Nit: they're adding Generics in 1.18 (not 1.8). Regarding
| "another 5 years": I'm not so sure. Go is very conservative
| about language changes. The type system didn't change at all
| from version 1.0 through version 1.17 (a 12-year period).
| mountainriver wrote:
| Type systems also allow people to understand your code, this is
| very important
| guidorice wrote:
| Best.Programmer.Art.Ever
| dnautics wrote:
| I didn't see the article touch on the "why" explicitly, but: zig
| really has the chance to square this circle for low level
| languages, since there is duck-typed type-inferenced-coercion in
| places where it makes sense. Completely correct about zig not
| necessarily being good for higher level stuff, but I think
| (dynamic) HLLs have been converging on dealing with this using
| static typechecking, with varying levels of success
| honkycat wrote:
| Great article, made me think! However, I think it needs to be
| trimmed down. Making your argument in the final paragraph of the
| article is not great.
|
| Hoist the "Final Words" section to the top and make it a "tldr"
| introduction, that way your reader can begin with a high level
| understanding of your argument, which you can hone and refine as
| you progress.
| ModernMech wrote:
| Ugh, I know I'm getting old when I don't understand the memes.
| arc619 wrote:
| This entire article can be summarised as "compile time stuff
| should use the same language as run time".
|
| I guess the author just hasn't encountered Nim before, where
| anything becomes compile time by just assigning to a const, and
| macros have access to the real AST without substitution. Macros
| also allow compile time type inspection, as they are a first
| class citizen rather than tacked on.
|
| The compile time print, AFAICT, already exists in Nim as the `&`
| macro in strformat. That lets you interpolate what you like at
| compile time, and supports run time values too.
| foxfluff wrote:
| > This entire article can be summarised as "compile time stuff
| should use the same language as run time".
|
| I think the message is more nuanced than that (otherwise
| wouldn't lisp with its homoiconicity and compile time macros
| fit the bill perfectly?). Idris uses the same language, but is
| still too complex. And Zig not general purpose enough. I don't
| want to put words in the author's mouth but I think the
| implication is that this is a large space to explore and we
| don't have a solution yet; there's nothing like "just make your
| language like this and it'll be good." They're just pointing
| out the problem they see, and some (non-)solutions to it.
| Hirrolot wrote:
| +1
| arc776 wrote:
| > I think the message is more nuanced
|
| I thought it was more nuanced too as they were explaining how
| integer types can be derived, until I finished the article,
| and they really did just seem to be complaining that there's
| a mismatch between compile time and run time.
|
| Dynamic types don't really solve the problems they mention as
| far as I can tell either (perhaps I am misunderstanding),
| they just don't provide any guarantees at all and so "work"
| in the loosest sense.
|
| > otherwise wouldn't lisp with its homoiconicity and compile
| time macros fit the bill perfectly?
|
| That's a good point, I do wonder why they didn't mention Lisp
| at all.
|
| > we don't have a solution yet
|
| What they want to do with print can, as far as I can see, be
| implemented in Nim easily in a standard, imperative form,
| without any declarative shenanigans. Indeed, it is
| implemented as part of the stdlib here:
| https://github.com/nim-
| lang/Nim/blob/ce44cf03cc4a78741c423b2...
|
| Of course, that implementation is more complex than the one
| in the article because it handles a lot more (e.g.,
| formatting and so on).
|
| At the end of the day, it's really a capability mismatch at
| the language level and the author even states this:
|
| > Programming languages ought to be rethought.
|
| I'd argue that Nim has been 'rethought' specifically to
| address the issues they mention. The language was built with
| extension in mind, and whilst the author states that macros
| are a bad thing, I get the impression this is because most
| languages implement them as tacked on substitution mechanisms
| (C/C++/Rust/D), and/or are declarative rather than "simple"
| imperative processes. IMHO, most people want to write general
| code for compile time work (like Zig), not learn a new sub-
| language. The author states this as well.
|
| Nim has a VM for running the language at compile time so you
| can do whatever you want, including the recursive type
| decomposition (this lib isn't implementing Peano arithmetic
| but multiprecision stack based bignums):
| https://github.com/status-im/nim-stint and specifically here:
| https://github.com/status-im/nim-
| stint/blob/ddfa6c608a6c2a84... func
| zero*[bits: static[int]](T: typedesc[Stuint[bits] or
| Stint[bits]]): T {.inline.} = ## Returns the zero
| of the input type discard func
| one*[bits: static[int]](T: typedesc[Stuint[bits]]): T
| {.inline.} = ## Returns the one of the input type
| result.data = one(type result.data)
|
| It also has 'real' macros that aren't substitutions but work
| on the core AST directly, can inspect types at compile time,
| and is a system language but also high level. It seems to
| solve their problems, but of course, they simply might not
| have used or even heard of it.
| chriswarbo wrote:
| I think the comparison between printf in Idris and Zig is a
| little off, since the Idris version defines an intermediate
| datastructure, and hence requires extra parsing and interpreting
| functions for it. That's a _nice_ approach, but the Zig version
| is operating directly on characters, so it 's a bit apples-to-
| oranges.
|
| We can get a more direct Idris implementation by inlining the
| parser (toFmt) into the interpreter (PrintfType). That lets us
| throw away `Fmt`, `toFmt`, etc. to just get:
| PrintfType : (fmt : List Char) -> Type PrintfType ('*' ::
| xs) = ({ty : Type} -> Show ty => (obj : ty) -> PrintfType xs)
| PrintfType ( x :: xs) = PrintfType xs PrintfType [] =
| String printf : (fmt : String) -> PrintfType (unpack
| fmt) printf fmt = printfAux (unpack fmt) [] where
| printfAux : (fmt : List Char) -> List Char -> PrintfType fmt
| printfAux ('*' :: fmt) acc = \obj => printfAux fmt (acc ++ unpack
| (show obj)) printfAux ( c :: fmt) acc = printfAux fmt
| (acc ++ [c]) printfAux [] acc = pack acc
| dvh wrote:
| Would printf even exist if C had sane strings?
| msla wrote:
| Some kind of formatting function would because sometimes, you
| really do need to print an integer with enough leading zeroes
| to fit in a five-digit field.
| foxfluff wrote:
| How is formatted printing related in any way to the internal
| representation of strings?
|
| printf is what you call when you want to print X in
| hexadecimal with at least two digits, left justified on an
| eight-character wide field. I don't see how the sanity of
| whatever string representation the programming language uses
| is relevant here.
| goldsteinq wrote:
| Except this version doesn't compile. I'm not sure that it's
| possible to get it to compile: type-level Idris is actually a
| _subset_ of Idris and pattern-matching non-ADTs is half-broken
| on the type level. You can also observe this problem in this
| simplified example: f : Char -> Type
| f '0' = Int f _ = Char g : (c : Char) ->
| (f c) g '0' = 0 g c = c
| lowbloodsugar wrote:
| "We might want to zip our car with their car..."
|
| We do or we don't. There is no "might". Spending money on "might"
| has been the death of many projects.
|
| If we didn't, and now we do, we could write a fn to map the car
| to parts, or we could define the car struct in terms of its
| parts, or we could just do away with the car altogether.
|
| But far more valuable would be an analysis of what changed about
| the requirements that the model no longer works.
|
| Now, don't get me wrong: I'd love a better language, and by
| better I mean "as fast as assembly but 'dynamic'". The problem is
| that, at the end of the day, all compilers are just "premature
| optimizations" or perhaps "willing premature optimizations". We
| could all be happily programming in smalltalk or build a runtime
| using predicate logic, but a) the number of people who could
| program in it is vanishingly small and b) it would be fucking
| slow. These languages don't solve a problem that I have, or
| rather they don't solve a problem that I don't already have a far
| better solution for. They solve a problem that academics have.
| [deleted]
| AtNightWeCode wrote:
| This is some kind of joke, right?
| seanw444 wrote:
| I really want to know where I can find quality programming memes
| like these. Not just the generic "haha language ___ is bad, nerd"
| memes.
| zmmmmm wrote:
| The problem I find with static typing is that it so easily leads
| you over-specifying the requirements / constraints. In fact, it
| makes such a virtue out of that over-specification that many
| people would consider it a best practice to do so.
|
| For example, perhaps my `calculate_price` function only depends
| on 2 attributes of the order which has 65 attributes. Am I
| creating a 2-element data type for that function to process? no!
| I'm specifying that it processes an Order data type, with all its
| 65 elements. But implicitly then I'm saying the function has 65
| input parameters of all these specific types and nobody can call
| it now without providing them all. What a pain! Huge amount of
| extra code, refactoring, unit testing, because of this.
|
| So either you end up with a cambrian explosion of micro-types or
| you have these way overspecified interfaces everywhere.
|
| Compare with dynamic languages (or structural typing, Go etc)
| that only care that things "quack like a duck". The
| calculate_price function doesn't care what object you give it, as
| long as it has the two attributes it needs. Now I can unit test
| `calculate_price` with a 2-element object rather than needlessly
| creating the 23 irrelevant required elements of a valid Order.
|
| I think a lot could be solved with culture shift. Where data
| types are really known and locked in, use the crap out of them.
| As soon as things get ambiguous or flexible, go right ahead and
| specify that your function takes a Map<String,Object>. If a
| useful concrete interface emerges at some point factor it out
| then. The problem is that this is really frowned upon in a lot of
| places.
| float4 wrote:
| https://imgflip.com/i/61wqdd
| brundolf wrote:
| You're putting structural types in the same boat as dynamic
| types, which I don't think is fair. Some of the most popular
| static type systems out there have structural typing, including
| Go (as you mentioned) and TypeScript. And that's not even
| getting into languages that do extensive type-inference,
| including TypeScript Haskell and ReScript (which also saves you
| from locking into over-broad contracts).
| yen223 wrote:
| One works with the type system they have, not the one they
| want. Out of the top 20 most popular languages according to
| Stack Overflow [0], TypeScript and Go are the only
| statically-typed languages that also have structural types
| support.
|
| [0]
| https://insights.stackoverflow.com/survey/2021#technology-
| mo...
| bqmjjx0kac wrote:
| > As soon as things get ambiguous or flexible, go right ahead
| and specify that your function takes a Map<String,Object>.
|
| Dear god, please don't. Some of the worst spaghetti code I have
| disentangled used this pattern. Typos in the string literals
| used as keys, object type mismatches, etc.
|
| If you only want some of the fields from another struct, you
| have a few options
|
| * Define another struct `FooArgs`. This is easy, and I
| (respectfully) reject the claim that nobody does it.
|
| * Just define your function to take those two fields directly.
| mdoms wrote:
| Or you could just take the two parameters you're actually using
| on your function. No new type, no need to pass your mega-
| object, just take two nice strongly typed arguments.
| armchairhacker wrote:
| "Why not add X feature? If people don't want to use X, they just
| don't, and there are basically 0 downsides."
|
| In theory this is true. If the compiler is decent, compile times
| and analysis shouldn't really be affected. Maybe libraries will
| use X but otherwise they would use a manual implementation of X
| anyways.
|
| But in practice developers misuse features, so adding a feature
| actually leads to worse code. It also creates a higher learning
| curve, since you have to decide whether to use a new feature or
| just re-implement it via old features. See: C++ and over-
| engineered Haskell. So each feature has a "learnability cost",
| and only add features which are useful enough to outweigh the
| cost.
|
| _But_ most features actually are useful, at least for particular
| types of programs. It 's much harder to write an asynchronous
| program without some form of async; it's much harder to write a
| program like a video game without objects. This may be
| controversial, but I really don't like Go and Elm (very simple
| languages) because I feel like have to write so much boilerplate
| vs. other languages where I could just use an advanced feature.
| And this boilerplate isn't just hard to create, it's hard to
| maintain because small changes require rewriting a lot of code.
|
| So ultimately language designers need to balance number of
| features with expressiveness: the goal is to use as few simple
| but powerful features to make your language simple but really
| expressive. And different languages for different people.
| Personally I like working with Java and Kotlin and Swift (the
| middle languages in the author's meme) because I can establish
| coding conventions and stick to them, C++ and Haskell are too
| complicated and it's harder to figure out and stick to the
| "ideal" conventions.
| preseinger wrote:
| Absolutely agree.
|
| All features are useful. That's table stakes. But usefulness is
| insufficient to warrant inclusion. How does a feature interact
| with all existing features? Are there ambiguities? Are there
| conflicts? A language is not a grab-bag of capabilities, it's a
| single cohesive thing that requires design and thought.
| acchow wrote:
| > But in practice developers misuse features, so adding a
| feature actually leads to worse code.
|
| I have found the opposite to be true. Missing features often
| leads to what one would call "design patterns". When the
| language adds official support to solve the problem you're
| trying to solve with that pattern, the code becomes clearer.
| throw10920 wrote:
| > But in practice developers misuse features, so adding a
| feature actually leads to worse code.
|
| Is that really a problem on the language's side, though? Devs
| are capable of mis-using _any_ feature, even extremely basic
| ones that almost every language has (variable names, for
| instance (although I 'm laughing in FORTH)). Code standards and
| code reviews are necessary tools in the first place because it
| doesn't matter what language you give a programmer - they're
| perfectly capable of constructing a monstrosity in it.
|
| I argue that preventing programmers from doing dumb things with
| well-designed language features (so, hygenic Scheme macros, and
| not raw C pointers) is a social and/or organizational problem,
| and it's better to solve that at that level than to try to
| solve it (inadequately) at a technical level.
|
| ("I keep dereferencing null pointers", on the other hand, _is_
| an example of a technical problem that can be solved on the
| technical level with better language design)
| erichocean wrote:
| FWIW, I've been developing code directly in MLIR recently, and in
| MLIR "Comparing types is cool" is indeed true.
|
| It's amazing what you can do when you have compiler
| transformations and targets always available.
|
| Suddenly, "little DSLs" (MLIR dialects) don't seem so bad, since
| they are defined the same way and map in semantically-sound ways
| to lower-level dialects. You can have dedicated dialects, like
| Halide, for doing something as concrete as image processing
| kernels.
|
| Oh, and you can output those kernels to both the CPU and GPU,
| including automatically introducing async functions, host-side
| sync barriers, etc. Good luck doing that automatically with a
| general purpose programming language and a combination of macros,
| AST manipulations, and derived types! You really need a compiler
| to stay sane.
|
| > _" Programming languages ought to be rethought."_
|
| Indeed.
| raphlinus wrote:
| Can I pick your brain on MLIR? It sounds awesome from what you
| describe, but I want to know more about whether it's
| specialized to machine learning types of workloads or whether
| it's good for more general things.
| preseinger wrote:
| > I cannot imagine a single language without the if operator, but
| only a few PLs accommodate full-fledged trait bounds, not to
| mention pattern matching. This is _inconsistency_ . . .
|
| How?
|
| > Sometimes, software engineers find their languages too
| primitive to express their ideas even in dynamic code. But they
| do not give up . . .
|
| Is this a failure of the language, or a failure of the engineer?
|
| > If we make our languages fully dynamic, we will win biformity
| and inconsistency,[^] but will imminently lose the pleasure of
| compile-time validation and will end up debugging our programs at
| mid-nights . . . One possible solution I have seen is dependent
| types. With dependent types, we can parameterise types not only
| with other types but with values, too.
|
| Types are a productive abstraction/model in programming
| languages. One of many. Each has its strengths and weaknesses;
| each is appropriate in some circumstances and not in others.
| Types are not the solution to all problems, any more than
| currying or OOP or whatever else is.
| gumby wrote:
| > > I cannot imagine a single language without the if
| operator...
|
| Production languages (like prolog or make) don't need an if
| statement or operator as selection is implicit when a
| production matches.
| oldsecondhand wrote:
| In Prolog :- is the if operator.
| Hirrolot wrote:
| Nice point, didn't know about that. My fail.
| jayd16 wrote:
| Shader languages are also hellbent on avoiding branches too
| so if is frowned upon and often not used. I could easily
| imagine not having it in a shader language.
| raphlinus wrote:
| This is very much changing. IMHO, doing shader language
| design today, you should give let the programmer express
| things in the most natural way possible, and let the
| compiler figure out whether to generate a branch or
| branchless code. Yes, often you want the latter, but
| compilers are pretty good at figuring that out.
| Zababa wrote:
| In the SML/OCaml world there's something like that: there is a
| difference between types and modules, and functions (from types
| to types) and functors (from module to module). Work was done on
| 1ML to unify everything: https://people.mpi-
| sws.org/~rossberg/1ml/. An extract:
|
| > In this "1ML", functions, functors, and even type constructors
| are one and the same construct; likewise, no distinction is made
| between structures, records, or tuples. Or viewed the other way
| round, everything is just ("a mode of use of") modules. Yet, 1ML
| does not require dependent types, and its type structure is
| expressible in terms of plain System Fo, in a minor variation of
| our F-ing modules approach.
|
| > An alternative view is that 1ML is a user-friendly surface
| syntax for System Fo that allows combining term and type
| abstraction in a more compositional manner than the bare
| calculus.
|
| On the other hand, from the "engineer" point of view, all
| abstractions melting into one may not be desirable. It's nice to
| be able to use weak abstractions for simple stuff and powerful
| abstractions for more powerful stuff. Being exposed to the full
| complexity of your language all the time sounds like a recipe for
| disaster.
| dandotway wrote:
| So whenever I have to study someone else's 'dynamic' python I
| encounter this sort of thing: def foo(bar, baz):
| bar(baz) ...
|
| What the heck is 'bar' and 'baz'? I deduce no more than 'bar' can
| be called with a single 'baz'. I can't use my editor/IDE to "go
| to definition" of bar/baz to figure out what is going on because
| everything is dynamically determined at runtime, and even
| grep -ri '\(foo\|bar\|baz\)' --include \*.py
|
| Won't tell me much about foo/bar/baz, it will only start a hound
| dog on a long and windy scent trail.
| lkrubner wrote:
| In Clojure, I tend to put pre and post assertions on most of my
| functions, which is useful for checking errors in the schema of
| runtime data (very useful when dealing with 3rd party APIs) but
| it also offers the documentation that you are seeking:
| (defn advisories [config] {:pre [
| (map? config) (:download-advisories-dir config)
| ] :post [ (map? %) ]
| } (let [ dir (:download-advisories-dir
| config) ] ;; more code here
| anyfoo wrote:
| And now imagine the compiler would actually enforce that
| practice, and you have static typing, with less boilerplate.
| maleldil wrote:
| How is this any better than static types?
| Jtsummers wrote:
| Pre/post conditions are complementary to a type system.
| They can ensure logical properties that may not be
| encodable in your underlying type system (that is,
| essentially every mainstream statically typed language).
| Such as the relationship between two values in a
| collection. Trivial example, if you have a range such as
| [x,y] where x < y must hold, how would you convey that in
| any mainstream type system?
| yakshaving_jgt wrote:
| The Haskell-y way to do this is to use a smart
| constructor[0].
|
| [0]: https://wiki.haskell.org/Smart_constructors
| Jtsummers wrote:
| The first part of that page demonstrates what amounts to
| pre/post conditions, but placed in the constructor. The
| range is checked dynamically, not statically.
|
| The second part is using Peano numbers to enforce the
| constraint. I guess you could try and force that into
| some mainstream languages, probably C++. With its
| template programming you could get something going in
| this vein, though I'm not sure how well it would work if
| the number were calculated at runtime rather than compile
| time. You'd still end up with a dynamic check somewhere.
| yakshaving_jgt wrote:
| The way that the value floats through the system is
| checked statically, and the program can (and should) be
| designed so that the value with the appropriate type
| cannot be constructed unsafely.
|
| If you need to statically check the construction of
| values in Haskell, there are things like refinement
| types[0].
|
| [0]: http://nikita-volkov.github.io/refined/
| Jtsummers wrote:
| > The way that the value floats through the system is
| checked statically, and the program can (and should) be
| designed so that the value with the appropriate type
| cannot be constructed unsafely.
|
| Except that in the first example from the first link you
| sent me, there is no _static_ guarantee that the inputs
| to the constructor are valid, thus the _error_ branch (it
| would be unnecessary if static guarantees could be made
| regarding the use of the constructor). And that was my
| point, that you still end up with _dynamic_ checks on the
| values which is where pre /post conditions step in to
| cover what static typing cannot (or, again, cannot easily
| in _mainstream_ languages, which would not be Haskell).
| yakshaving_jgt wrote:
| Is there any reason you didn't address the _second_ link
| that I shared?
| [deleted]
| foxfluff wrote:
| Static languages unfortunately don't save you from that. You
| find automatically inferred types, or types that refer to some
| abstract interface or template-class-mess but you have no idea
| where the actual implementation lives until you compile with
| RTTI and run it under a debugger... and as tfa posits, people
| working with the limitations of static languages often end up
| reinventing a dynamic structure.
|
| Is this somehow supposed to relevant to the posted article or
| did you just want to start a tangentially related dynamic-vs-
| static flame war here in the comments section?
| Diggsey wrote:
| > Static languages unfortunately don't save you from that.
|
| While you still _can_ make a static language that is
| confusing, it 's a lot harder... I challenge you to write a
| function signature in Rust that is both:
|
| 1) Useful
|
| 2) As opaque as the python signature above.
|
| > You find automatically inferred types
|
| A minority of static languages do type inference in function
| signatures. I think it's a bad idea for exactly the same
| reason the python code is bad. On the other hand, _every_
| dynamic language allows you to omit any information about a
| type signature.
| jacobn wrote:
| They usually save you from that particular pitfall, but not
| always of course.
|
| Static vs dynamic makes for such difference in the detailed
| workflow, both in terms of changing existing code & in terms
| of writing new/(more) from scratch code, yet they can both be
| quite fruitful, and can both be abused in absurdum.
|
| It seems like people naturally fall into one of the two camps
| (either by personality or by training), and the other side
| just seems kind of insane: "how can you even work that
| way!?". Then culture and idioms emerge over time and
| strengthen the tribalism.
|
| I've gone back and forth between the two over the course of
| my career, and it's quite a mind-shift when switching, with a
| fair bit of pain involved ("but it would be so easy to do
| this in [old language]", or "what the hell is this garbage
| anyway!?") and then eventually it settles in and it's not all
| painful, all the time ;)
|
| (Going back and forth between Scala and Python right now, so
| this hit a bit of a nerve)
| hota_mazi wrote:
| > Static languages unfortunately don't save you from that.
| You find automatically inferred types,
|
| Oh yes, they do. Even inferred, the types are there and
| pretty easy to locate, even if you're not using an IDE.
| foxfluff wrote:
| The types are there.. but you don't know which one it is
| that your program is dealing with. You could have dozens of
| implementations for any given abstract interface. One gets
| picked up at run time.
| Diggsey wrote:
| You don't need to know which one, because the abstract
| interface tells you how to use it...
| foxfluff wrote:
| That's the theory. Works great when there are no bugs and
| everything's been designed just right. In that world you
| could wipe implementations from memory because you won't
| ever need to dive in..
|
| Very often I'm looking at code and "how to use the
| interface" is not a question I'm looking to find answers
| for.
| Diggsey wrote:
| Some information is a lot better than none. In some cases
| you might want to know what implements the interface:
| that information is also statically available. In Rust,
| you can look at a trait and see what types implement that
| trait.
|
| If you need to know _exactly which implementation is
| being used in a particular context_ then maybe you
| shouldn 't be using an interface, but should be using the
| concrete type?
| foxfluff wrote:
| > If you need to know exactly which implementation is
| being used in a particular context then maybe you
| shouldn't be using an interface, but should be using the
| concrete type?
|
| Look again at the original comment by dandotway. It's not
| a question of "what type should I be using here", nor is
| it "how should I use this interface", but "what do I need
| to do to understand this code?". Even if an abstract
| interface is used correctly and is the right thing to do,
| you still need to understand the code before you can
| (debug|rewrite|extend|whatever) it.
|
| And it's a pipe dream to say you can look _just_ at the
| interface. Something blows up, is it the interface used
| wrong? Maybe, maybe not? Is it the interface implemented
| wrong? Maybe, first you need to know what implementation
| you 're looking at. Subtle interactions between abstract
| and concrete send you spelunking through the layers when
| you're debugging or trying to extend the interface to
| account for something the original author didn't
| anticipate, and often the devil (in the details) comes
| from concrete implementations.
| dmitriid wrote:
| > to some abstract interface or template-class-mess
|
| And traits! "Oh look, this functionality is implemented in a
| trait implemented by a trait implemented by a trait
| implemented by what you're looking at. Maybe"
| socialdemocrat wrote:
| That is why I like dynamic languages like Julia better because
| they use type annotations more frequently.
| vanusa wrote:
| _What the heck is 'bar' and 'baz'?_
|
| So there's no docstring? And the actual variables are that
| random and indecipherable?
|
| Sounds like the problem is that you're tasked with looking at
| code written by someone who is either inexperienced or
| fundamentally careless. When dealing with reasonably maintained
| codebases, this kind of situation would seem pretty rare. In
| modern python we now have type hints of course, which have
| helped quite a lot.
| timeon wrote:
| I do not want to argument against Python. This is more like
| off topic / side note.
|
| What I like about Rust is that it even checks code in your
| 'docstring'. So it is easier to keep it maintained.
| maxwell86 wrote:
| > In modern python we now have type hints of course, which
| have helped quite a lot.
|
| I had to laugh, hard.
|
| If your Python program uses any library whatsoever, chances
| are that library won't have types, so you can't really use
| them.
|
| Even super widely used libraries like numpy don't have good
| support for types, much less any library that consumes numpy
| for obvious reasons.
| vanusa wrote:
| _I had to laugh, hard._
|
| It's fine if you want to insulate yourself. But I don't see
| that you're making much of a point here.
| arc619 wrote:
| They're probably laughing because a) you're suggesting
| manually doing the work static typing does in a dynamic
| language because its untenable not to for large projects,
| and b) you can't easily add type hints to other people's
| libraries.
| vanusa wrote:
| No - (a) is not what I'm suggesting. And (b) while
| disappointing, just doesn't slow one's work down very
| frequently in daily practice.
|
| Look, I just don't buy the suggestion that static typing
| magically solves a huge set of problems (or that it does
| so without imposing negative tradeoffs of its own -- the
| very topic of the original article). Or that dynamic
| languages are plainly crippled, and that one has to be a
| kind of a simpleton not to see this obvious fact.
| yakshaving_jgt wrote:
| > I just don't buy the suggestion that static typing
| magically solves a huge set of problems
| // compiles and runs, but does bad things function
| foo(x, y) { someDangerousEffect(); return
| x + y; } -- does not compile; huge sets
| of problems magically solved foo :: Int -> Int ->
| Int foo x y = someDangerousEffect >> pure $ x + y
| vanusa wrote:
| A problem but not a huge one in practice.
|
| And you're neglecting the part of my statement you
| conveniently truncated.
| yakshaving_jgt wrote:
| > not a huge one in practice.
|
| Our experiences have been wildly different :)
| vanusa wrote:
| Yeah, life sometimes gets that way.
| arc776 wrote:
| > just doesn't slow one's work down very frequently in
| daily practice.
|
| Well, maybe you don't feel it slows you down, but it is
| manual work you must do to get a reliable product only
| because of dynamic typing. Not only that, but you have to
| then refer to these docs to check you're not creating a
| type calamity at some nebulous point down the run time
| road. Static languages just won't let you mess that up,
| and often have utilities to generate this documentation
| for you at no effort.
|
| > I just don't buy the suggestion that static typing
| magically solves a huge set of problems
|
| Static typing _really does_ "magically" solve a whole
| class of problems without any negative tradeoffs,
| assuming the language has decent type inference.
|
| Not all problems, but a specific class of them that you
| _should_ do extra work to guard against in dynamic
| languages. Whether that is extra documentation that has
| to be reliably updated and checked, or run time code to
| check the types are what you expect at the point of use.
|
| Take for example JavaScript, where typing is not only
| dynamic, but weak. Numbers in particular can be quite
| spicy when mixed with strings as I'm sure you know.
| Strong, static typing forces you to be explicit in these
| cases and so removes this problem entirely.
|
| By the way, no one's saying anyone is a simpleton. The
| reality is our field is wide and varied, and different
| experiences are valid.
|
| Dynamic languages can do some things that static
| languages can't. For example, you can return completely
| different types from different execution paths in the
| same function.
|
| This has been something that has confused me when reading
| Python, but it does make it easier for stuff like tree
| parsing. In a static language you need to specify some
| variant mechanism that knows all data possibilities ahead
| of time to allow this. From my perspective the dynamic
| typing trade off isn't worth these bits of 'free' run
| time flexibility, but YMMV! It really depends what arena
| you're working in and what you're doing.
| duped wrote:
| Coming from any statically typed language to Python or
| JavaScript codebases this plagues me. Virtually every project
| I have seen suffers from this.
|
| Function names and doc comments describe behavior, not
| argument and return types.
| vanusa wrote:
| _Not argument and return types._
|
| When conventions are followed they do exactly that,
| actually. And unless the code is a complete trainwreck,
| it's pretty easy to tell what the return type is, even
| without explicit annotation in the docstring.
| duped wrote:
| What conventions are those, besides Hungarian notation
| (which I don't think I've ever seen in Python)?
| vanusa wrote:
| PEP 257, and local conventions in certain projects.
|
| What all comes down to is: if you really have people on
| your project writing code like the foo/bar/baz example
| way up above - then you have problems way bigger than
| static type checks can possibly help you with.
| duped wrote:
| Wouldn't it be something if there existed tooling that
| enforced this level of discipline and checked its
| validity before executing any code such that you didn't
| rely on the entire ecosystem to adhere to the same
| standards and remove that as a source of ambiguity...
| arc619 wrote:
| In other words, in Python you have to rely on your colleagues
| manually writing documentation, and if they don't you're out
| of luck and they're 'bad developers' and potentially the
| whole product is affected.
|
| In static languages this simply isn't a problem. Types are
| checked for consistency at compile time and you don't have to
| rely on people toiling on this busy work.
|
| Not to say documentation isn't necessary, or good, but isn't
| something you need to create working programs because
| otherwise no one knows wtf any variable is without running
| the program.
| prewett wrote:
| What I've found is even worse: on medium-size personal
| projects I inevitably pass the wrong type to a function (or
| refactor and forget to change a call somewhere). Generally
| it does not fail until somewhere else, which relies on it
| being a certain type. So even though I know what my
| functions should take, I still spend a bunch of time
| tracking down the problems, which involves a printing out a
| lot of stuff because the error happened outside the current
| call stack. This is something a static type system would
| just prevent for me, and I've basically decided that
| anything beyond really simple stuff is actually faster for
| me to write in a statically typed language. (Examples in
| the past are a parser for a static site generator with a
| mostly Turing-complete language, and 3D model generators)
| vanusa wrote:
| No, actually I advocate static typing approaches for
| precisely the reasons you give. As does Python, since years
| and years ago.
|
| I'm just saying that core problem it solves -- "bad
| developers" -- is going bite you no matter what (if not
| addressed at the source). And that supposed magical
| efficacy of measures designed to protect against it is
| somewhat overstated.
| lkuty wrote:
| Indeed. An interesting reading is https://lexi-
| lambda.github.io/blog/2020/01/19/no-dynamic-typ...
| yakshaving_jgt wrote:
| This is one of my favourite blog posts on the Internet and I
| implore every programmer to read it.
|
| > The claim is simple: in a static type system, you must
| declare the shape of data ahead of time, but in a dynamic
| type system, the type can be, well, dynamic! It sounds self-
| evident, so much so that Rich Hickey has practically built a
| speaking career upon its emotional appeal. The only problem
| is it isn't true.
| mountainriver wrote:
| Yup, I find this completely insane behavior to think that you
| somehow benefit from types not being there.
|
| You just make it way harder for people to understand your code
| and contribute to it.
| jan_g wrote:
| To be fair, in recent years I've worked on a number of
| Typescript projects and it was common for developers to use
| `any`, `Object`, `() -> Promise<void>`, etc. Not super
| helpful.
|
| Though in my experience, sane code structure and informative
| comments trump everything else when it comes to understanding
| big and unknown codebase. I still shudder when I think about
| working years ago on various Java codebases (mostly business
| IT systems). What a convoluted mess of n-levels deep
| interface hierarchies. Types? Yeah, but good luck unraveling
| what exactly is happening in the runtime.
| dgb23 wrote:
| The example is entirely ridiculous. Types are names too. Does
| foo(bar: baz) solve the issue? Languages are there to convey
| meaning.
| commandlinefan wrote:
| It doesn't just make the code harder to read, it makes it run
| slower, too. Static typing provides some compile-time
| guarantees about what's going to go where, so the compiler
| can make a lot of simplifying assumptions that speed things
| up.
| [deleted]
| vanusa wrote:
| Insane, no - it's just a tradeoff.
|
| I agree that on balance type signatures are better -- and
| that's why modern Python has evolved to incorporate them. But
| they aren't a magic cure-all, and do they impose a
| significant tax of their own.
| tus666 wrote:
| Oh yeah the easiest code in the world to read is some
| contorted type system and function signatures that look like
| hieroglyphics that you need a PHd in CS to comprehend.
|
| Python is easy to grok, and if you have programmers writing
| code like bar(foo,baz) then the problem is not Python. You
| can write crap in any language.
|
| Unit tests do much of what typing checks anyway ... and
| here's the thing ... you NEED unit tests no matter what. No
| typing system can tell you that you wrote > when you should
| have written <.
| hota_mazi wrote:
| > Python is easy to grok,
|
| It's not, though, it just gives you that illusion.
|
| The code might be easier to read but it's harder to
| understand and to modify safely because of the absence of
| type annotations.
| teakettle42 wrote:
| > Oh yeah the easiest code in the world to read is some
| contorted type system and function signatures that look
| like hieroglyphics that you need a PHd in CS to comprehend.
|
| You can also make a book easier to read by ripping out all
| its pages.
|
| If you eliminate the content you need to read to understand
| something, what have you actually made easier?
|
| > No typing system can tell you that you wrote > when you
| should have written <.
|
| There are many that can; e.g. via SMT-decidable refinement
| types, or even full undecidable dependent types coupled
| with automated solvers and manual proofs.
| pchangr wrote:
| Yeah, I always say that python is an amazing language to
| prototype and terrible language to scale precisely because
| it lets people write the usual terrible code and then gives
| you the freedom to make it even worse.
| laumars wrote:
| > Python is easy to grok
|
| It's what you're used to. I personally find Python horrible
| to read because I used to a whole different class of
| programming languages. But I'm sure some of my code might
| be hard to read by others who aren't used to that
| particular programming language too.
|
| > Unit tests do much of what typing checks anyway ... and
| here's the thing ... you NEED unit tests no matter what.
|
| Some, not all. Strictly typed languages are handy when it
| comes to refactoring and unit tests can sometimes fail
| there if the design is being changed enough that the unit
| tests need rewriting too.
|
| > No typing system can tell you that you wrote > when you
| should have written <.
|
| Not technically true. Some languages with a richer set of
| types and operator overloading could have code written to
| detect that sort of thing. But I do get your point that
| unit tests are import too.
|
| I've been programming for > 30 years and in dozens of
| different languages. In that time I've felt strictly typed
| languages make larger and more mature code based slightly
| easier to maintain. While loosely typed languages are
| easier for smaller and/or younger code based. But largely
| it boils more down to personal preference than anything.
|
| I will caveat that by saying the fact that Python supports
| type annotations should be telling that even dynamic
| languages benefit from a stricter approach to typing.
| anyfoo wrote:
| > Oh yeah the easiest code in the world to read is some
| contorted type system and function signatures that look
| like hieroglyphics that you need a PHd in CS to comprehend.
|
| People who just learn programming probably think the same
| about whatever language they are learning.
|
| > No typing system can tell you that you wrote > when you
| should have written <.
|
| The more the compiler can figure out for you, the quicker
| problems can be identified and fixed. I stopped using
| python altogether, because it was just infuriating to have
| the tiniest mistakes blowing up in spectacular and
| inscrutable ways. Mixing up values of complex types often
| does not fail at the actual site of the error, but much,
| much later. Sometimes literally later in time, as in hours,
| days, or months until you get an obscure "FooType does not
| Bar" error, and how the thing in question ever became a
| FooType is inscrutable at that point. If the result even is
| a runtime error at all! (Bonus points if your production
| database is now full of junk as well.[1])
|
| The unit test did not catch it because it did not test the
| offending composition of classes and functions. Meanwhile,
| a compiler would have caught it immediately: "The thing
| you're doing here leads to your data structures being
| nested wrong."
|
| When I started using async/await in python, at first it was
| just over, since in plain python that introduces another
| layer of typing without any assistance whatsoever. Then I
| discovered mypy which actually lets me do some amount of
| static typing in python, and it was very enjoyable and now
| python is back on the table for smaller projects.
|
| There is a reason Haskell has the reputation of "if it
| compiles, it works". There is a reason why system
| programmers that work on critical systems are jealously
| eyeing Rust if their shop still does C.
|
| By the way, dependent type systems absolutely can tell you
| if you wrote > instead of <. But since that usually comes
| at the expense of not being Turing complete anymore, it's
| more used for very critical systems, or for theorem
| provers.
|
| [1] Yes sqlite, I'm looking at you. The decision to make
| database column dynamically typed, and hence have for
| example an INTEGER column silently accept data that is very
| much not an INTEGER at all, caused me some grief on a
| widely deployed system once.
| duped wrote:
| Typing allows you to specify the expected behavior in terms
| of input/output structure of algorithms in such a way that
| they can be statically verified without writing unit tests
| or manual checking code in the source, allowing your unit
| tests to check behavior by value rather than by value _and_
| structure. The equivalent of type checking is not unit
| testing, but fuzzing.
|
| Python is not easy to grok at all, when you consider you
| have to grok implementations to understand what they are
| supposed to do, and require extensive runtime debugging to
| figure out if it is behaving as expected before you can
| even write unit tests.
|
| Compare to decent statically typed languages, which have
| quicker write/debug cycles since checking type definitions
| is faster than checking code behavior, and the structural
| unit testing is covered automatically by the compiler.
|
| It's like getting more than 50% of your programs' test
| coverage, for free!
| [deleted]
| aidos wrote:
| Well, you could put the types on these days.
|
| But also, there's nothing stopping the code from being much
| clearer about its intention than this weirdly contrived example
| (I have a lot of code and most the function names are pretty
| unique). And surely you want to search for 'foo(' to find
| invocations.
| rightbyte wrote:
| My worst developer experience ever was trying to make changes
| to a custom build system with functions exactly like that.
| Injector or decorator pattern, or what ever it is called.
|
| I just gave up and introduced globals and used as flags for
| some places in the code.
|
| To make things even more fun, big parts of the system was
| written in 2.7 called from runtime generated bat files from 3.4
| as remnants of a rewrite and the consultant that had his
| funding cut.
| nicoburns wrote:
| The real power of dynamic languages is being able to do:
| const foo = JSON.parse(arbitraryJsonString);
|
| and not having to worry about the structure up front.
| ironman1478 wrote:
| But the thing is, don't you have to worry about structure?
| You have to unpack the elements from the JSON, so you will
| need to encode its structure explicitly, which includes type
| information. The only reason this would be useful is if
| you're just shuttling data to another API that expects a dict
| structure (that will then validate everything) and not JSON
| and you aren't really doing any real work yourself.
| preseinger wrote:
| Structure is a virtue, not a vice. By doing this you're
| subverting your own interests.
| pdpi wrote:
| Structure is a tool. Like any tool, it can be misused or
| overused.
|
| For anything even remotely production-y I'll always prefer
| explicitly parsing JSON into a known structure, but there's
| a lot of value in in being able to do some exploratory
| scripting without those constraints.
| preseinger wrote:
| Yes! Exploratory scripting is a categorically different
| thing than programming, though, I think.
| convolvatron wrote:
| not necessarily. there is a bottom up school of thought
| that encourages people to noodle around and construct
| primitives by playing in the domain, and then
| interactively composing those primitives into larger and
| larger systems.
| preseinger wrote:
| Yeah, that's true. It's a judgment call for sure, but
| I've always found that angle on things to be self-
| subversive. The best programmers I know are all bottom-up
| learners, not top-down.
| naasking wrote:
| > The real power of dynamic languages is being able to do:
| const foo = JSON.parse(arbitraryJsonString); and not having
| to worry about the structure up front.
|
| That's not power, that's a shotgun aimed at your crotch whose
| trigger is connected to a cosmic ray detector.
| dandotway wrote:
| +NaN For comments that make me laugh out loud for duration
| T>2.0 seconds, I wish HN provided a way to
| transmute/sacrifice one's past karma points into additional
| +1 mod points.
| cheriot wrote:
| There's something to this. I love featurefull type systems,
| but I've seen engineers try to parse JSON the "right" way in
| Scala, get frustrated, and blame the entire concept of
| statically typed languages. Elm manages to make this user
| friendly so perhaps it's "only" a matter of compiler messages
| and API design?
| mountainriver wrote:
| You can do that in static languages too by just parsing into
| a map
| tus666 wrote:
| What's the type definition of the map out of interest?
| nyberg wrote:
| const Json = union(enum) { null,
| number: f64, bool: bool, string:
| []const u8, array: []const Json,
| object: HashMap([]const u8, Json), };
|
| Usually it's a tagged union of the base JSON types which
| can easily be consumed by most statically typed languages
| or a variant of it.
|
| EDIT: added "tagged"
| pas wrote:
| In TS JSON is usually Record<string, unknown>.
| tus666 wrote:
| Well unknown is not a type (by definition), so you have
| just stepped outside of a type system, which is very
| common in TS if I understand.
| pas wrote:
| Unknown is a top type in the TS type system. It serves
| the very important role of "here you need to apply some
| pattern matching and validation" and then you can make
| sure that you can continue working in a type safe
| environment. TS has a lot of facilities that help you
| with this (from the usual narrowing things, typeof and
| instanceof guards, control flow analysis, and at the end
| of the list there are the big guns like this thing called
| type predicates which basically allows you to wrap these
| checks and "casts" in nice reusable functions).
|
| There are also recursive types that help you model JSON,
| but knowing that it's an arbitrary deep nested map/list
| of maps/lists and number and bool and string mixed like a
| Bloody Mary cocktail doesn't really help :)
|
| With NestJS it's very easy to add decorators/annotations
| to fields of a class, and the framework handles
| validation (throws HTTP 422 with nice descriptions of
| what failed) and then in your controller you can again
| work in a type safe environment.
|
| https://www.typescriptlang.org/docs/handbook/release-
| notes/t...
|
| https://www.typescriptlang.org/docs/handbook/2/narrowing.
| htm...
| staticassertion wrote:
| Json = Map<string, Json>
| steveklabnik wrote:
| In Rust:
| https://docs.serde.rs/serde_json/value/enum.Value.html
| HideousKojima wrote:
| In C# I can just parse it to a Dictionary<string, object>,
| and I can do that _without_ destroying the usability of the
| rest of the language.
| tomp wrote:
| What if the JSON represents a list, or an int?
|
| Also, how do you then access nested objects, like
| data['key'][0]['attr'] in Python?
| tus666 wrote:
| Well in Python everything is an object, so that type
| definition holds true for Python as well :P
| jayd16 wrote:
| You can parse it to the dynamic type too. Everyone is
| happy...right?
| yakshaving_jgt wrote:
| You can trivially do exactly the same thing in Haskell, so I
| think you're suggesting that dynamic languages have no "real
| power".
| adwn wrote:
| Rust: let foo: serde_json::Value =
| serde_json::from_str(arbitraryJsonString)?;
|
| There, just as powerful [1]. But you know what's even more
| powerful? After you've done your dynamic checks, you can do
| this on the entire JSON tree, or on a subtree:
| let bar: MyStaticType = serde_json::from_value(foo)?;
|
| and you get a fully parsed instance of a static type, with
| all the guarantees and performance benefits that entails.
|
| [1] _Value_ represents a JSON tree:
| https://docs.serde.rs/serde_json/enum.Value.html
| commandlinefan wrote:
| I thought the real power of dynamic languages was being able
| to do things like: eval('alert("hello, ' +
| userInput.name + '!")')
| woodruffw wrote:
| Every static language that I know of also supports this --
| you can parse into a sum type of `JsonAny` (or whatever),
| where `JsonAny` is one of `Null | Number | String | List[Any]
| | Dict[String, JsonAny]`.
|
| The API then becomes a runtime fallible one, which is
| perfectly sound.
| laumars wrote:
| That's not a capability unique to dynamic languages
| sandofsky wrote:
| When that JSON payload changes (intentionally or not), you
| will run into a mysterious problem in some unrelated area of
| your code. It will be significantly more expensive to fix
| than failing fast at the point of parsing.
| suzzer99 wrote:
| But depending on the situation, that problem may never
| happen. I'm not a big fan of introducing complexity to
| guard against code screwups in the same codebase.
| sandofsky wrote:
| If you work somewhere APIs never change, and your backend
| people never ship bugs, that's great, but I feel it's
| rare.
|
| In a modern language like Swift, this sort of type safety
| is pretty simple. You declare a struct, and slap a
| _Codable_ on it. struct User:Codable {
| var id:String var username:String }
|
| Now you'll just error out if the data is invalid. And no
| more forgetting that _id_ is a string, not a number. And
| as a bonus, your IDE will auto complete properties as you
| type them.
|
| Even when revving on a pre-production API, I find it an
| incredible time saver. Decided to change a property name
| from "username" to "screen_name"? Your IDE will rewrite
| will gladly rewrite the hundred old references in
| seconds.
|
| I find this way simpler that passing strings to black
| boxes.
| cdaringe wrote:
| The hardest bugs are the rare bugs. Common and frequent
| problems are easy bugs to fix. These subtle or rare
| changes are those which should be feared. If you model
| only that which you support, finding the source of the
| problem becomes much easier.
| arc776 wrote:
| In Nim you can even convert JSON to static types!
| https://nim-
| lang.org/docs/json.html#to%2CJsonNode%2Ctypedesc...
|
| Now you get type checking on JSON at compile time :)
| valcron1000 wrote:
| What's 'foo' and what you can do with it?
| carnitine wrote:
| An arbitrary object? What else would arbitrary JSON parse
| into? Then you can access its properties, like with any JS
| object.
| solox3 wrote:
| > I can't use my editor/IDE to "go to definition" of bar/baz
|
| I use "Find Usages" on foo to see where it is used. Once you
| see where it is used, you know what the types can be. It's not
| great, but it's also something that can be progressively
| remedied.
|
| In the event that the function is not as trivial as your
| example suggests, the author should have written a docstring to
| help you understand what it is trying to do, in addition to
| type annotations that will make it more readable.
|
| In this example, bar can either be a function, a class, or any
| object with __call__(), so the type information is less
| important in this case, than actual docstrings that express
| intent.
| dleslie wrote:
| I'm unfamiliar with one of the language logos in the meme graph
| at the bottom: what's the red swooshy thing beside zig?
| andrenth wrote:
| It's the Idris logo https://www.idris-lang.org/
| [deleted]
| [deleted]
| [deleted]
| devit wrote:
| Yeah, the current problem is that Idris code is far less
| efficient than Rust code, because Idris boxes everything and
| erases all types, and also Idris's support for borrowing seems
| less powerful than Rust (it lacks first-class mutable borrows as
| far as I can tell).
|
| It seems that fixing this is a research problem, which would lead
| to the holy grail of programming languages, i.e. an ultimate
| language that is as expressive as Idris and as efficient as Rust,
| and is thus essentially perfect.
| preseinger wrote:
| Expressiveness is not an unambiguous net good -- more
| expressiveness is not a priori better. Expressiveness carries
| costs of comprehension and coherence that need to be
| appropriately weighed in the contexts where the language will
| be applied.
|
| Programming languages are not theoretical things. They're
| concrete, practical tools that _enable_ other stuff.
| Engineering, not science.
| ImprobableTruth wrote:
| How would you define expressiveness (as its commonly used, so
| a definition where Turing complete languages can have
| different expressiveness) if not as how much something can be
| simplified and thus aiding comprehension, rather than
| detracting from it?
|
| >Programming languages are not theoretical things. They're
| concrete, practical tools that _enable_ other stuff.
| Engineering, not science.
|
| You can't escape theory, engineering is applied science.
| preseinger wrote:
| Increasing expressiveness of a language necessarily
| increases its complexity. Comprehension is important but
| it's a function of "the whole stack" -- language and
| program both.
|
| > engineering is applied science.
|
| Absolutely. But the metrics are different.
| siknad wrote:
| > an ultimate language that is as expressive as Idris and as
| efficient as Rust, and is thus essentially perfect.
|
| Are both Idris's expressiveness and Rust's efficiency (given
| stronger guarantees) perfect? Aren't theese languages really
| complex both to learn and to write? There are poblems without a
| solution, perfect and unique to all of them.
| dwohnitmok wrote:
| > Idris's support for borrowing seems less powerful than Rust
| (it lacks first-class mutable borrows as far as I can tell).
|
| Depends on what you mean. Idris's notion of multiplicities
| essentially subsumes Rust's borrowing (there's some differences
| with affine vs linear types), so I can't think off the top of
| my head of things that you can ensure with Rust that you can't
| with Idris, but Rust has a lot more quality of life
| improvements that make things less clunky (also having a GC,
| Idris can get away with a lot less need for borrowing in the
| first place).
| zozbot234 wrote:
| The Prusti effort to endow Rust with proof-carrying code is
| also worth mentioning. There are some reasons to expect this
| approach to be more fruitful than an actual extension of
| dependently-typed languages, since the type system features of
| Rust itself are hard to integrate with dependent types. (At
| best, it might be somewhat feasible to use the latter in the
| `const`, compile-time evaluated subset of the language.)
| preordained wrote:
| Having used Clojure for a while now, I will say having 90% of
| things be a primitive, map, or vector goes a long way in and of
| itself. A lot of types concocted in a more conventional language
| just don't need to exist, IMO, and they create so much baggage
| around themselves.
| Zababa wrote:
| You know what they say about people with hammers.
| zmmmmm wrote:
| Hmm, how well does this scale though? you are passing around
| these giant maps of vectors of tuples and then you pass it to
| someone unfamiliar with the code, how the hell do they know
| what's in there? Is the order price the first element of the
| tuple or the second? What happens when I refactor things and
| now all the tuple elements shift over one? Surely you'll end up
| writing just as much in documentation as you would have to
| specify the types?
|
| Currently working my way through some complex Python code
| written in that style and it's completely impossible to
| understand it. In fact, the only way I can actually do it is
| transforming all these ad hoc data structures into proper types
| so I can make sense of it.
| adamddev1 wrote:
| I wonder where TypeScript would fall on this language continuum?
| dnautics wrote:
| My guess: It wouldn't because this is about static languages.
| Typescript is still a dynamic language with a very smart
| (probably best-in-class at this point in time) compile-time
| typechecker/static analysis tool.
| batrachos wrote:
| I dislike the phrase 'dynamic language' and especially dislike
| the phrase 'static language'. We should say 'dynamically typed'
| or 'statically typed', because 'static' languages are the site of
| major dynamism.
| chriswarbo wrote:
| I think 'dynamic language' is appropriate here, since it's not
| _only_ talking about types; it 's largely talking about macros,
| pre-processors, reflection, etc. too.
|
| Also, the main argument is that separating features into those
| used at compile-time (AKA static) and run-time (AKA dynamic) is
| necessarily creating separate _languages_ (i.e. a "static
| language", which may involve types, macros, preprocessors,
| etc.; and a "dynamic language", which may involve memory
| allocation, branching, I/O, etc.)
| tempodox wrote:
| This article spends many words to say, "there is no silver
| bullet".
|
| But dynamically typed languages produce at least the same amount
| of accidental complexity, just in different ways.
| lambdasquirrel wrote:
| Indeed, the article has it backwards. The types are always
| there. Your program will fail at runtime if it's not correct.
| The type system merely surfaced that.
|
| Complexity in the types happens when the type system isn't
| expressive enough. Or when you're trying to do something that
| would make the compiler try to solve the halting problem.
|
| To that last point, this is why the PLT community has pushed in
| the direction that Agda / Idris has. Kind of like how we
| realized years (decades?) ago that we didn't need pointer
| arithmetic, there's been a realization that "total" isn't
| actually that helpful, and it's okay if we didn't have
| languages that could express the halting problem.
| skybrian wrote:
| That's the hope, but saying there's something wrong is
| insufficient. The compile-time errors need to be
| understandable, or it's just going to be frustrating.
|
| Maybe we should judge compile-time constraint systems by how
| easy it is for the library author to add good error messages
| for misuse?
| Hirrolot wrote:
| > This article spends many words to say, "there is no silver
| bullet".
|
| Rather "I believe there is a silver bullet, but I don't know
| where yet". Probably I am too naive!
___________________________________________________________________
(page generated 2022-01-19 23:01 UTC)