13 ms·
> It's probably why techs like Javascript, MongoDB, Ruby on Rails, Java, PHP, MySQL, Wordpress, etc. have gotten widespread adoption despite numerous technical
by jakub_h 10y ago
> It's probably why techs like Javascript, MongoDB, Ruby on Rails, Java, PHP, MySQL, Wordpress, etc. have gotten widespread adoption despite numerous technical flaws. They have a very low barrier to getting something useful up on screen, and feed peoples' need for instant gratification.
It might not be just about that. Sometimes you don't know what you're doing, and you have to find out first. What use is it if you painstakingly design and craft a perfectly engineered program, only to find out that you actually needed something else? Being able to experiment is precious. Being forced to handle everything before the thing even compiles is not all that useful at times. There's a reason why interactive environments like Lisp and Smalltalk don't force you to define all functions that could plausibly get called (and when they do get called, you can even add them on the fly and continue).
- brandonbloom 10y agoBig +1 to this. The takeaway for me is to develop languages and tools that scale with the domain-expertise curve. That is, as you learn more about both the problem and solution, you can evolve your implementation without having to start over or get many things right from the start. The goal should be to both tighten the feedback loop and provide a smooth path from tinkering towards production grade.
- pka 10y agoStatic languages, in particular Haskell, PureScript, etc, are unparalleled when it comes to fast prototyping or design space exploration. The compiler can show you whether your idea even makes sense before writing big chunks of business code. How? By building the structure/hard stuff first and letting the compiler verify it while leaving out the uninteresting parts. A very simple example. Let's say you have some data model with people, addresses, whatever. You want to see what you need to do to get a list of addresses for some group of people. You start by defining the structure of your data model (this can depend on DB schema, business requirements, whatever): data Person = Person { name :: String, city :: String } data Address = Address { city :: String, zipcode :: String, street :: String } Now, go straight to the hard part. No need to fuck around with DB connections, mock data, whatever: getAddresses :: [Person] -> [Address] getAddresses = undefined Leave out the implementation for now. So probably we are gonna need a way to get an Address for a Person. Ok: addressForPerson :: Person -> Address addressForPerson = undefined Again, leave out the implementation. Now go back to `getAddresses` and see if we can implement it now: getAddresses = map addressForPerson Cool, the compiler doesn't complain. So far we've verified that if we have a function like `addressForPerson` we can safely assume that our design is going to work. Now we can go back to `addressForPerson` and implement that. But turns out, we only have a city stored in Person, so the best we can do is get back a list of potential addresses for each person. So we need to adjust that type signature: addressForPerson :: Person -> [Address] addressForPerson = undefined Leave out implementation, it's trivial so no need to spend time on it now. But now the compiler complains that `getAddresses` doesn't make sense anymore! We are saying that, having a list of people, we can get a list of addresses, but that's not true anymore. We found out that we can only get a list of potential addresses, so we have to adjust the type signature. getAddresses :: [Person] -> [[Address]] Now every use of `getAddresses` will have to be adjusted to the new requirements. Etc etc. Granted, this is a very simple example, but I hope it illustrates how static languages facilitate extremely fast prototyping. Sure, a Lisp REPL is very very nice, but in my personal experience it's no match for a type system. We didn't even have to mock data to start experimenting! (And yes, Haskell, OCaml, PureScript, Elm etc all have a REPL too).
- PeCaN 10y agoWhile I agree, getting used to that mode of thought takes time. IME you have to write a lot of Haskell (or OCaml) before it becomes a very fast prototyping language. Of course, it's 100% worth the investment, and I prefer prototyping in OCaml or Mercury to basically anything else, but it certainly wasn't like that from the first time I picked up OCaml.
- jakub_h 10y ago> "Static languages, in particular Haskell, PureScript, etc, are unparalleled when it comes to fast prototyping or design space exploration. The compiler can show you whether your idea even makes sense before writing big chunks of business code. How? By building the structure/hard stuff first and letting the compiler verify it while leaving out the uninteresting parts." Why shouldn't I be able to do this on a meta-level? There does not appear to be any restriction on, for example, Lisp code regarding the use of problem solvers and provers. The same should go for other metalanguages (perhaps even better, without forcing me into specific semantics). (Likewise, generative solutions might reduce this problem simply by performing a sequence of correctness-preserving transformations from a higher-level specification. That would shift the burden to checking the specification, which would require a custom checker anyway. Haskell's capability to check or automatically derive stuff seemed limited the last time I looked at it.)
- pka 10y agoAgreed. My comment wasn't aimed that much at Haskell in particular (even if it's my language of choice), but rather the idea of using structure/types for fast experimentation, rather than just a REPL. Also, using a type checker to wholly check your Lisp code basically makes it a static language, the difference then is pretty much philosophical I'd say.
- jakub_h 10y agoRecently I've found myself having severe problems with seeing clear borders between many concepts. IF expressions, type-based dispatching, matching, generic functions, predicate dispatching...a lot of it basically boils down to the same thing, only expressed in different ways. The problem is that the same thing goes for type systems in my head. Types define sets of values, but if your language hardcodes what kinds of sets of values you can express, chances are that you'll have to express whatever residual distinction between values there is (that you can't encode into the value type) into one of the above-mentioned decision-making processes, and at that point, they go sort of out of sight of the typing/proving machinery. I'd rather avoid someone forcing me into using a hardcoded type system, among other reasons because I'm contemplating a (perhaps novel?) search-based program/expression-manipulating system that wouldn't play well with any fixed typing system. For one thing, the types of expression results (being return values of operators applied to input expressions or programs) can easily be emergent (is the resulting expression linear? Is it an odd function? Is it analytically integrable? Etc.) and not easily arrived at in something ML-like. If I had to use something like Haskell, I'd still be rolling out my own type system/prover on top of the existing language implementation anyway. However, types in general (as something you can compute with in the meta-program) would be absolutely mandatory for a system like this (for example, some algorithms or operations can have more desirable refinements that work only on subsets of values - consider determinants of triangular matrices, with triangular matrices being an emergent type - and I absolutely want to find out if such a thing is available for the specific problem being transformed), just not types that any specific existing language offers, to my knowledge. So it's not that I dismiss the notion of proving things about programs - that is a vital thing to have - but the ability to impose restrictions on a program from the outside seems much more flexible to me than going for some fixed type discipline that could force me to straddle inconvenient fixed walls in the structure of a complicated program (such as this transformation system that would search the tree of possible transformations for a result with some desirable properties, with the transformations concisely expressed in some very-high-level DSL source code).
- nilkn 10y agoThis doesn't feel consistent with my experience. Significantly restructuring a large and complex piece of infrastructure is never going to be easy, but I don't think a strong static type system makes it any harder. If anything, it makes it significantly easier, just like a strong battery of unit tests would. A sufficiently expressive type system will guide you and hold your hand over the course of the refactoring. It will determine for you the precise cascading effect of any code changes you make. And if you forgot to write a unit test to handle some edge case, the chances are pretty decent that the type system will detect it for you anyway. In terms of writing a new piece of software whose design you haven't fleshed out yet, my approach in a language like Haskell is in fact very experimental, but quite different from something like Python. Rather than writing lots of little tests and trying to get bare-bones functionality implemented passing those tests, I often completely ignore tests and let the type system guide me. I try to flesh out the types that my program will need at a high level and just go from there. I call it "type-driven development", and it's extremely useful for early-stage exploratory programming, because it lets you play around with the structure of the program without worrying about any details. This is only possible in a language those type system is powerful enough to actually encode nontrivial aspects of a program's high-level structure. I make sure at every point that I have a codebase that passes the type checker. It may not be interesting to actually run it at an early stage, but in many ways that actually makes exploratory problem solving even easier. In many cases, this approach will reveal architectural issues early on that you may not have realized existed until you'd already written thousands of lines of code in something like Python. The real point is that either way you will have to face those issues if you want to end up with a working piece of software. But it's a lot easier to face them early on, when little code depends on them, rather than later on.
- nostrademons 10y agoI think there's a distinction in exploratory programming between code with known inputs & outputs but unknown data structures & algorithms, vs. code with known algorithms but unknown inputs and outputs. The former encompasses situations like contract development, signal processing, control & embedded systems, compilers, maintenance programming, etc. I've found that strictly typed languages like Haskell, Ada, or Rust can be very helpful here, even for exploratory programming. In this situation, you know (and have little control over) the data that goes into your program and the requirements that it must meet, and if it doesn't do so, it's because you made a mistake somewhere. But many types of exploratory programming involve unknown inputs or unknown outputs. Think of data mining or scientific computing, where the issue is often that you have unclean data and your task is to figure out how it's messed up and how to fix it as quickly as possible. Or UI software (both web and mobile) and tech entrepreneurship, where the big unknown is often how the user will react to the software and where they will find value in it. I've found that dynamic languages are great for this, because the whole point is you don't know what the data will look like, let alone how it will be structured. It was interesting to me that the two alternatives you mentioned were "write lots of little tests" or "let the type system guide what needs to be fixed". For a lot of the exploratory programming I've been doing lately, the most efficient approach is just "let it break". Go make your changes, and if some part of the system doesn't work anymore, just take note of that. Because the act of letting things break gives you important information. If you don't care, then obviously that feature wasn't very important to the system to begin with, and you should rip it out entirely. If you do care, you should fix it, and then write some tests to make sure it doesn't break again. This way, you can build a rough rank ordering of how important features are, so that when you're faced with engineering trade-offs (and you always are), you have a solid, experience-based intuition about what you really need to support and what you can let slide.