Live data from Hacker News

Six Years of Professional Clojure

engineering.nanit.com

211–220 of 254 posts

Re: Six Years of Professional Clojure

#211

Earlier quoted context omitted.

Any statically-typed language with generics can express that by parameterising the request type with the body type. A bodiless request is then just Request[Nothing] (or Request[Unit] if your type system doesn't have a bottom type). Accessing the headers just requires an interface which all static languages should be able to express.

(1) note that “statically-typed language with generics” excludes a lot of statically typed languages, including C and Go (at least pre generics). (2) this misses the meat of the question which is how to express that (eg) a GET request doesn’t come with a body and a POST request does. I suppose that you’re suggesting that one registers a url handler with a method type and that forces the handler to accept responses of…

1) Looking at the TIOBE index, all the static languages I recognised on there are: C,C++,C#,Visual Basic,Go,Fortran,Swift,Delphi,Cobol,Rust,Scala,Typescript,Kotlin,Haskell and D. Of these C and Go are the only two that don't appear to support generics so I don't think this approach excludes a lot of static languages.

2) If you want to distinguish GET and POST requests statically then you just need a type for them e.g.

    GetRequest implements Request { }
if you don't need to do this then you can just add a method field and use a single type for both. Either way you don't need to use sum types so a language like Java can express it.

3) Yes you'll have to make functions that don't care about the body type generic so this approach could become unwieldy if you have a few such properties you want to track.

Re: Six Years of Professional Clojure

#212

Earlier quoted context omitted.

On the contrary the The NonEmpty type is fundamental to the approach in that example since it contains in the type the property being checked dynamically (that the list is non-empty). The nonEmpty function is a simple example of the 'parse don't validate' approach since it goes from a broader to a more restricted type, along with the possibility of failure if the constraint was not satisfied. The restriction on the N…

You are misunderstanding the system. You can organize the logic into whatever containers you want, but the essence of the system cannot be changed. You are already handling a `Maybe` type because it's possible for your input to not exist. Because the first implementation of `head` also returns a `Maybe`, it is possible to "bind" them together (I'm leaving out `IO` because I am both unsure of the syntax[0] and it is i…

This line:

    maybeCache >= head >> initializeCache)
is doing exactly what the post is arguing against. getConfDirs is validating the list is non-empty but the [FilePath] list it contains does not encode that information. Now you immediately have to handle the possibility of a missing value from head that you already know cannot happen. This isn't too apparent here since you've combined it into a single expression but if you need to pass the confDirs list to any other part of the program they will also have to continually handle the possibility of the list being empty even though you already checked for that possibility. Now every function that interects with the confDirs list will have to include (Maybe a) in its return type unnecessarily. The post is not suggesting you can remove Maybe entirely but it has moved it to a single point in the program (the point where the config dirs list is checked for emptiness) and removed it everywhere else. Your approach must continually guard against an impossible condition everywhere the dirs list is accessed because you discard the property you checked for in getConfDirs.

The monadic operators make it convenient to propagate missing values through a chain of operations but they are not the primary benefit of an explicit Maybe type. Much like IO, the benefit of having an explicit Maybe type is when you _don't_ have it since its absence represents more information at that point in the program. Likewise a (NonEmpty a) contains more informatation than [a] which consequently makes the implementation of head more informative.

The parsers in this approach have types like

    a -> Maybe b
where type b contains the extra information extracted by the parser. Your getConfDirs function only contains a function with type

    [a] -> Maybe [a]
so isn't parsing in the same way.

Re: Six Years of Professional Clojure

#213

Earlier quoted context omitted.

> didn't alert me until I tried running the program That's because that's not how Clojure developers normally work. You don't do changes and then "run the program". You start your REPL and send expressions from your editor to the REPL after you've made a change you're not sure about. So you'd discover the missing argument when you call the function, directly after writing it.

Interesting. How exactly that looks? Do you have files opened in your editor, change them then go into previously opened repl, and just call the functions and the new version of those function runs?

Almost. I don't actually ever navigate to the REPL process itself. I have my source files open in my editor, and when I want to evaluate something, I select the expression to evaluate and press my shortcut to evaluate it. Then my editor shows the results of the evaluation either inline or in a separate window next to the call.

So when I later call the function I created for example, it'll use the new evaluated code instead of the old. If I'm happy, I save the file, everything reloads from there while keeping the same state.

Re: Six Years of Professional Clojure

#214
post #156

Earlier quoted context omitted.

Here's what I've noticed with my tests and dynamic languages. I'll get type errors that static typing would have caught. However those errors occur in places I was missing testing of actual functionality. Had I had the functionality tests, then the type error would have been picked up by my tests. And had I just had static typing, the type system would not have been enough to prove the code actually works, so I would…

> type systems are totally unable to express algorithms You don't know much about types if you think that. As for dynamic typing "helping" you to find code that you need to write tests for: There are already far more sophisticated static analysis tools to measure code coverage.

  doubler :: Num a => [a] -> [a]
  doubler xs = take 2 xs
Passes the type checker, thanks type system! /s

I like static typing, but static typing advocates seriously overstate how much protection the type system gives you. Hickey really said it best: "We used to say 'If it compiles it works' and that's as true now as it was then."

As for dynamic typing "helping find code to write tests", that's not a feature, it's a huge downside. Neither side is perfect, but in my experience the benefits of the static checker are overblown since I need to write tests anyways. And also like you say, there's a variety of great static analysis tools you should be using as well.

Re: Six Years of Professional Clojure

#215
post #209

Earlier quoted context omitted.

I feel like there's a missing axis in the static/dynamic debate: the language's information model. In an OOP language, types are hugely important, because the types let you know the object's ad-hoc API. OOP types are incredibly complicated. In lisps, and Clojure in particular, your information model is scalars, lists, and maps. These are fully generic structures whose API is the standard Clojure lib. This means that…

Smalltalk is "OOP x dynamic". CLOS is "OOP x dynamic". Common Lisp has arrays, structures and stack allocation. IDE were invented from Smalltalk and Lisp development experience.

CLOS is wierd since it sits in a multiparadigm language, so CL really spans boxes. JS is the same as well depending on how you use it. Smalltalk is a great example since it really did manage to do OOP x dynamic in a much better way and its worth wondering why Smalltalk pulled it off where Ruby feels like a nightmare. I suspect it has to do with focusing on the message passing aspect.

So no, it's not a perfect model, but I think its more informative than looking at languages on a one dimensional static / dynamic axis.

Re: Six Years of Professional Clojure

#216
post #48

> Pure functions make code design easier: In fact, there’s very little design to be done when your codebase consists mostly of pure functions. Ummm... I am a little bit fearful about your codebase. If you don't see the need for designing your FP system it probably mostly means it is being designed ad hoc rather than explicitly. If you are trying to compare to OOP system done right, you will notice that this includes…

> If you don't see the need for designing your FP system it probably mostly means it is being designed ad hoc rather than explicitly.

Why does FP seem to imply that things are designed ad hoc rather than with purpose? I've been working exclusively with FP codebases for the last 5 year, and all designs have been by identifying the domain model and implement it with purpose, with a plan.

> includes a lot of work in identifying domain model of your problem

FP does not exclude creating a domain model of your problem, discovering names and so on, not sure why you think so? Love to hear the reasoning behind this view you have.

Re: Six Years of Professional Clojure

#217
post #156

Earlier quoted context omitted.

> type systems are totally unable to express algorithms You don't know much about types if you think that. As for dynamic typing "helping" you to find code that you need to write tests for: There are already far more sophisticated static analysis tools to measure code coverage.

doubler :: Num a => [a] -> [a] doubler xs = take 2 xs Passes the type checker, thanks type system! /s I like static typing, but static typing advocates seriously overstate how much protection the type system gives you. Hickey really said it best: "We used to say 'If it compiles it works' and that's as true now as it was then." As for dynamic typing "helping find code to write tests", that's not a feature, it's a huge…

I'm not sure when did this become about typing systems promising you'll never write tests? IMO nobody says that.

Let me give you one example.

When I'm coding in Rust and I forget to match on one of my sum type's variants, the compiler will immediately yell at me.

When I'm coding in Elixir, the compiler doesn't care if I do exhaustive pattern matching because it doesn't know all possible return values. In these conditions it's extremely easy to not write code that deals with a return value that appears rarely.

That's one of the values of static typing for me.

Re: Six Years of Professional Clojure

#218

Earlier quoted context omitted.

doubler :: Num a => [a] -> [a] doubler xs = take 2 xs Passes the type checker, thanks type system! /s I like static typing, but static typing advocates seriously overstate how much protection the type system gives you. Hickey really said it best: "We used to say 'If it compiles it works' and that's as true now as it was then." As for dynamic typing "helping find code to write tests", that's not a feature, it's a huge…

I'm not sure when did this become about typing systems promising you'll never write tests? IMO nobody says that. Let me give you one example. When I'm coding in Rust and I forget to match on one of my sum type's variants, the compiler will immediately yell at me. When I'm coding in Elixir, the compiler doesn't care if I do exhaustive pattern matching because it doesn't know all possible return values. In these condit…

That was meant as a response to tsss apparently overvaluing his type checks.

The pattern matching example is one that often comes up talking about typing. Yes it's great that the type checker finds all the places you didn't deal with your new sum type varient...except here's the rub. All that code was working just fine before. Your static type checker is forcing a bunch of code that never needed to know or care about certain values onto all places where you used pattern matching. I don't think this speaks to the value of static typing, I think it suggests that pattern matching is a bad idea that leads to overly coupled code where parts of the system that really shouldn't need to know about each other are now forced to deal with situations they don't care about.

Re: Six Years of Professional Clojure

#219

Earlier quoted context omitted.

I'm not sure when did this become about typing systems promising you'll never write tests? IMO nobody says that. Let me give you one example. When I'm coding in Rust and I forget to match on one of my sum type's variants, the compiler will immediately yell at me. When I'm coding in Elixir, the compiler doesn't care if I do exhaustive pattern matching because it doesn't know all possible return values. In these condit…

That was meant as a response to tsss apparently overvaluing his type checks. The pattern matching example is one that often comes up talking about typing. Yes it's great that the type checker finds all the places you didn't deal with your new sum type varient...except here's the rub. All that code was working just fine before. Your static type checker is forcing a bunch of code that never needed to know or care about…

> Your static type checker is forcing a bunch of code that never needed to know or care about certain values onto all places where you used pattern matching.

It's not "forcing" anything, you are evolving your program and the compiler is helping you not play a whack-a-mole by actually telling you every place that must be corrected in order to account for the change.

Wasn't aware that evolving a project is called forcing. :P

> I think it suggests that pattern matching is a bad idea that leads to overly coupled code where parts of the system that really shouldn't need to know about each other are now forced to deal with situations they don't care about.

That's a super random statement, dude. If a sum type change makes 7 places in your code not compile then obviously those pieces of code do care about it -- you wouldn't write it that way if it didn't. Nobody put a gun on your head forcing you to include the sum type in these places in the code just because, right?

Overall I am not following your train of thought. You seem to be negatively biased. I've seen from my practice only benefits by enforcing exhaustive pattern matching. Many times I facepalmed after I got a compiler error in Rust and was saying "gods, I absolutely would've missed that if I wrote it in a dynamic language".

Re: Six Years of Professional Clojure

#220

Earlier quoted context omitted.

You are misunderstanding the system. You can organize the logic into whatever containers you want, but the essence of the system cannot be changed. You are already handling a `Maybe` type because it's possible for your input to not exist. Because the first implementation of `head` also returns a `Maybe`, it is possible to "bind" them together (I'm leaving out `IO` because I am both unsure of the syntax[0] and it is i…

This line: maybeCache >= head >> initializeCache) is doing exactly what the post is arguing against. getConfDirs is validating the list is non-empty but the [FilePath] list it contains does not encode that information. Now you immediately have to handle the possibility of a missing value from head that you already know cannot happen. This isn't too apparent here since you've combined it into a single expression but i…

I understand what the author is doing. I said this earlier but it bears repeating, the author seems to be more concerned with having a concrete type than simpler code. A reference to `Maybe Cache` is good enough (and preferred). The top-level of your program is precisely where you want to have the flexibility to deal with the above.

Furthermore, my example is a much better illustration of the axiom ("Parse don't validate") than what the author is doing -- which is more like "Parse and validate".

You need to clarify "continuously guard". Sure you have to invoke methods like:

    maybeCache >> useCache // map 
instead of:

    maybeCache |> useCache // not sure how Haskell pipes
Is that too difficult? The `Maybe` monad is specifically designed so that you don't have to continuously guard against the possibility of the value not existing. That is, you can "map", "bind" and "apply" functions to the value as if it always exists (and it handles the situation when the value doesn't). I also included a `case` block within which you can be statically certain a value of type `Cache` is available if you really need it.

The purpose of `Maybe` is to simplify code that needs to deal with a value that might not exist. Attempting to organize your code to avoid using `Maybe` is, by definition, going to be more cumbersome than simply leaning into the construct (that's what it's for!). It also better-illustrates how "parse don't validate" should work. Using an exception to guard against an invariant is... validating not parsing.

You don't need to defend the author here. It's just a matter of fact the the code provided could be organized differently according to a more idiomatic usage of `Maybe`, and therefore a more illustrative example of their own point. The choice to exemplify something else is unfortunate and the thrust of this entire comment thread -- I felt like I had to say something now seeing that link a second time.

Post reply on HN