<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Hacker News: lkitching</title><link>https://news.ycombinator.com/user?id=lkitching</link><description>Hacker News RSS</description><docs>https://hnrss.org/</docs><generator>hnrss v2.1.1</generator><lastBuildDate>Fri, 01 May 2026 08:30:12 +0000</lastBuildDate><atom:link href="https://hnrss.org/user?id=lkitching" rel="self" type="application/rss+xml"></atom:link><item><title><![CDATA[New comment by lkitching in "Why Clojure?"]]></title><description><![CDATA[
<p>I've already shown that type hints do not constitute type checking:<p><pre><code>  (defn f [^String s] (.length s))
  (f 3)
</code></pre>
is a valid Clojure program that fails at runtime with a cast error.<p><pre><code>  class X { public static int f(String s) { return s.length(); } }
  X.f(3)
</code></pre>
is not a valid Java program at all. Clojure compilation generates bytecode to dispatch dynamically and all but the most basic checks are handled at runtime by the JVM. This is fundamentally different to the static type checking that languages like Java and Scala do. It's not that Clojure is hiding something from Java, but rather that it isn't doing the considerable amount of effort the Java type checker does to analyse the program before execution. This is by design - Clojure has deliberately avoided adding a static type system in favour of things like spec.</p>
]]></description><pubDate>Tue, 25 Feb 2025 10:30:53 +0000</pubDate><link>https://news.ycombinator.com/item?id=43170195</link><dc:creator>lkitching</dc:creator><comments>https://news.ycombinator.com/item?id=43170195</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=43170195</guid></item><item><title><![CDATA[New comment by lkitching in "Why Clojure?"]]></title><description><![CDATA[
<p>Of course Clojure has to ultimately be compiled into a native format for the host platform, bytecode in the case of the JVM implementation, but that doesn't require type checking in the same way Java does.<p>Clojure functions are compiled into implementations of clojure.lang.IFn - you can see from <a href="https://clojure.github.io/clojure/javadoc/clojure/lang/IFn.html" rel="nofollow">https://clojure.github.io/clojure/javadoc/clojure/lang/IFn.h...</a> that this interface simply has a number of overloads of an invoke method taking variable numbers of Object parameters. Since all values can be converted to Object, either directly for reference types or via a boxing conversion, no type checking is required to dispatch a call. With a form like<p><pre><code>  (some-fn 1, "abc", (Object.))
</code></pre>
the some-fn symbol is resolved in the current context (to a Var for functions defined with defn), the result is cast (not checked!) to an instance of IFn and the call to the method with required arity is bound. This can go wrong in multiple ways: the some-fn symbol cannot be resolved, the bound object doesn't implement IFn, the bound IFn doesn't support the number of supplied arguments, the arguments are not of the expected type. Clojure doesn't check any of these, whereas the corresponding Java code would.<p>Protocol methods just get compiled into an implementation of IFn which searches for the implementation to dispatch to based on the runtime type of the first argument, so it doesn't introduce static type checking in any way.</p>
]]></description><pubDate>Tue, 25 Feb 2025 00:16:31 +0000</pubDate><link>https://news.ycombinator.com/item?id=43166566</link><dc:creator>lkitching</dc:creator><comments>https://news.ycombinator.com/item?id=43166566</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=43166566</guid></item><item><title><![CDATA[New comment by lkitching in "Why Clojure?"]]></title><description><![CDATA[
<p>Protocols do not work like Java interfaces or classes. Their methods are compiled into regular functions which lookup the implementation to use at runtime based on the runtime type of the receiver. Compilation will check for the named function but doesn't do any further checking. Given the following protocol and implementation:<p><pre><code>  (defprotocol P
    (method [this ^Integer i]))

  (extend-protocol P
    String
    (method [s i] (.substring s i)))
</code></pre>
both (method "test" "call") and (method 1 2) will be accepted by the compilation phase but will fail at runtime.<p>Of course there's no requirement for Clojure code to be AOT compiled anyway so in that case any name errors will still only be caught at runtime when the compilation happens.<p>Type hinted bindings are only converted into a cast and are not checked at compilation time either e.g.<p><pre><code>  (defn hinted [^String s] (.length s))
  (hinted 3)
</code></pre>
will be accepted but fail at runtime.<p>deftype is only used for Java interop an is also not a form of type checking. The methods will be compiled into Java classes and interfaces, but the implementations defer to regular Clojure functions which are not type checked. You can only make use of the type information by referencing the compiled class files in Java or another statically typed language, using them from Clojure will not perform type checking.</p>
]]></description><pubDate>Mon, 24 Feb 2025 17:20:36 +0000</pubDate><link>https://news.ycombinator.com/item?id=43162154</link><dc:creator>lkitching</dc:creator><comments>https://news.ycombinator.com/item?id=43162154</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=43162154</guid></item><item><title><![CDATA[New comment by lkitching in "Why Clojure?"]]></title><description><![CDATA[
<p>Neither defprotocol nor deftype introduce static typing into Clojure. Errors in their usage are not checked statically and are only discovered at runtime.</p>
]]></description><pubDate>Mon, 24 Feb 2025 11:19:13 +0000</pubDate><link>https://news.ycombinator.com/item?id=43158244</link><dc:creator>lkitching</dc:creator><comments>https://news.ycombinator.com/item?id=43158244</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=43158244</guid></item><item><title><![CDATA[New comment by lkitching in "Why Haskell?"]]></title><description><![CDATA[
<p>That just means the semantics of the language are defined by whatever the default implementation does. It's a big stretch to conclude that means Rust 'was' OCaml in some sense when the compiler was written with it. Especially now the Rust compiler is written in Rust itself.</p>
]]></description><pubDate>Thu, 12 Sep 2024 15:22:43 +0000</pubDate><link>https://news.ycombinator.com/item?id=41521953</link><dc:creator>lkitching</dc:creator><comments>https://news.ycombinator.com/item?id=41521953</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=41521953</guid></item><item><title><![CDATA[New comment by lkitching in "Why Haskell?"]]></title><description><![CDATA[
<p>I'm not convinced the implementation language of the compiler counts as a feature of the Rust language. If the argument is that Rust wouldn't have been invented without the original author wanting a 'systems OCaml' then fine. But it's possible Rust would still look similar to how it does now in a counterfactual world where the original inspiration was Haskell rather than OCaml, but removing the Haskell influence from Rust as it is now would result in something quite different.</p>
]]></description><pubDate>Thu, 12 Sep 2024 14:36:12 +0000</pubDate><link>https://news.ycombinator.com/item?id=41521502</link><dc:creator>lkitching</dc:creator><comments>https://news.ycombinator.com/item?id=41521502</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=41521502</guid></item><item><title><![CDATA[New comment by lkitching in "Why Haskell?"]]></title><description><![CDATA[
<p>Which OCaml features exist in Rust but not Haskell? The trait system looks very similar to Haskell typeclasses, but I'm not aware of any novel OCaml influence on the language.</p>
]]></description><pubDate>Thu, 12 Sep 2024 14:02:55 +0000</pubDate><link>https://news.ycombinator.com/item?id=41521104</link><dc:creator>lkitching</dc:creator><comments>https://news.ycombinator.com/item?id=41521104</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=41521104</guid></item><item><title><![CDATA[New comment by lkitching in "Monads are like burritos (2009)"]]></title><description><![CDATA[
<p>The way in which monads are monoids isn't really helpful for understanding them for progamming.</p>
]]></description><pubDate>Mon, 03 Jun 2024 09:50:24 +0000</pubDate><link>https://news.ycombinator.com/item?id=40560974</link><dc:creator>lkitching</dc:creator><comments>https://news.ycombinator.com/item?id=40560974</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=40560974</guid></item><item><title><![CDATA[New comment by lkitching in "Java JEP 461: Stream Gatherers"]]></title><description><![CDATA[
<p>Since this defines an interface, does this solve the null problem? e.g.<p><pre><code>    Maybe<String> foo = null;
    foo.map(s -> "bar");
</code></pre>
explicit pattern matching is usually discouraged anyway though, and you can do this now with Optional<p><pre><code>    Optional<String> foo = Optional.empty();
    String message = foo.map(s -> "Hello " + s).orElse("Fine, leave me hanging");
</code></pre>
Other languages like Scala also have an Option.fold method for this specific case.</p>
]]></description><pubDate>Fri, 03 Nov 2023 12:48:10 +0000</pubDate><link>https://news.ycombinator.com/item?id=38127911</link><dc:creator>lkitching</dc:creator><comments>https://news.ycombinator.com/item?id=38127911</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=38127911</guid></item><item><title><![CDATA[New comment by lkitching in "EU corporate tax proposal would hit Ireland hardest"]]></title><description><![CDATA[
<p>How is this related to Brexit? Ireland have had a low corporation tax rate for the last 20 years, long before the Brexit vote.</p>
]]></description><pubDate>Thu, 12 Oct 2023 12:19:39 +0000</pubDate><link>https://news.ycombinator.com/item?id=37856179</link><dc:creator>lkitching</dc:creator><comments>https://news.ycombinator.com/item?id=37856179</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=37856179</guid></item><item><title><![CDATA[New comment by lkitching in "(next Rich)"]]></title><description><![CDATA[
<p>Assuming you're referring to *1, *2, *3 and *e, these are only defined within the REPL and are never used in real programs.</p>
]]></description><pubDate>Sat, 05 Aug 2023 18:24:11 +0000</pubDate><link>https://news.ycombinator.com/item?id=37014953</link><dc:creator>lkitching</dc:creator><comments>https://news.ycombinator.com/item?id=37014953</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=37014953</guid></item><item><title><![CDATA[New comment by lkitching in "Parse, don't validate (2019)"]]></title><description><![CDATA[
<p>The OP is contrasting between a 'validation' function with type e.g.<p><pre><code>    validateEmail :: String -> IO ()
</code></pre>
and a 'parsing' function<p><pre><code>    validateEmail :: String -> Either EmailError ValidEmail
</code></pre>
The property encoded by the ValidEmail type is available throughout the rest of the program, which is not the case if you only validate.</p>
]]></description><pubDate>Tue, 07 Mar 2023 14:39:39 +0000</pubDate><link>https://news.ycombinator.com/item?id=35055815</link><dc:creator>lkitching</dc:creator><comments>https://news.ycombinator.com/item?id=35055815</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=35055815</guid></item><item><title><![CDATA[New comment by lkitching in "Parse, don't validate (2019)"]]></title><description><![CDATA[
<p>The post is suggesting that parsing and validation are different things, since the output of a parser captures the properties being checked in the type, and validation does not. Downstream consumers of validated input cannot rely on the properties that were validated since the representation type doesn't encode them e.g. the non-emptiness of a list.</p>
]]></description><pubDate>Tue, 07 Mar 2023 12:01:00 +0000</pubDate><link>https://news.ycombinator.com/item?id=35054377</link><dc:creator>lkitching</dc:creator><comments>https://news.ycombinator.com/item?id=35054377</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=35054377</guid></item><item><title><![CDATA[New comment by lkitching in "How does it know I want CSV? – An HTTP trick"]]></title><description><![CDATA[
<p>Arguably the resource is the dataset of stock exchanges, and the CSV representation is forced to omit all the metadata but the HTML representation isn't.</p>
]]></description><pubDate>Tue, 17 Jan 2023 17:13:40 +0000</pubDate><link>https://news.ycombinator.com/item?id=34415244</link><dc:creator>lkitching</dc:creator><comments>https://news.ycombinator.com/item?id=34415244</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=34415244</guid></item><item><title><![CDATA[New comment by lkitching in "Scala isn't fun anymore"]]></title><description><![CDATA[
<p>There's been a fair amount of churn in the ecosystem, even if the language itself has been very stable. Leiningen was ubiquitous 5-6 years ago, but if you
switched to using deps for dependency management you lost the ability to run tests or build uberjars and had to manually re-create these on a per-project
basis. Now there's tools.build, but that also requires you to manually write essentially the same tasks for basic functionality in each project. Leiningen
and tools.deps also seem to resolve depedencies differently, so you can run into issues simply by migrating from one to the other.</p>
]]></description><pubDate>Wed, 14 Sep 2022 16:12:06 +0000</pubDate><link>https://news.ycombinator.com/item?id=32839245</link><dc:creator>lkitching</dc:creator><comments>https://news.ycombinator.com/item?id=32839245</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=32839245</guid></item><item><title><![CDATA[New comment by lkitching in "Wipeout (PSX and Windows Source)"]]></title><description><![CDATA[
<p>All those links are to the first game, not Wipeout 3.</p>
]]></description><pubDate>Sun, 27 Mar 2022 18:25:49 +0000</pubDate><link>https://news.ycombinator.com/item?id=30822665</link><dc:creator>lkitching</dc:creator><comments>https://news.ycombinator.com/item?id=30822665</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=30822665</guid></item><item><title><![CDATA[New comment by lkitching in "Selecting a programming language can be a form of premature optimization"]]></title><description><![CDATA[
<p>Sorry for the confusion, but regardless of whether you call it casting, conversion, or something else, it's still trivial to do safely in one directly and impossible to do safely in the other. Given your fixation on the terminology I assume you've now accepted this.<p>> But via an optional type you guarantee that it's handled<p>It's not 'handled', which is the point. There's no default value so you have no option but to propagate the missing value. Callers that establish the precondition still have to deal with an Optional value even though it can't be empty. Callers which establish the precondtion through the types receive a more precise type and don't have this problem. Callers that establish the precondition in the more simply-typed version (Int, Int) -> Int also receive an Int. Only your version imposes the imprecision on the return type.<p>> There is nothing wrong with using a Maybe monad here. There is zero reason why you need to throw an exception.<p>The Optional[Value] returned from a map lookup is used to incidicate the possibility of a missing key, which is an expected outcome of the operation itself. Passing a null map represents a structural error in the construction of the program at the point the call is made. If you represent both in the same type you can't distinguish these two cases.<p>> You now have a hole in subtraction as 3 - 3 would be a singularity.<p>There's no hole here, subtraction on NonZero just has to return Int instead.<p>>  If it comes from IO anything goes, you have to be prepared for Anything<p>Yes, if it comes from user input you have to establish the property dynamically. Encoding the property in the argument type forces you to actually do it (whether statically or dynamically), and help you push the constraint to the highest level. Putting the optionality in the return type doesn't force you do do this, and imposes a cost on every call that actually does.</p>
]]></description><pubDate>Thu, 25 Nov 2021 17:17:42 +0000</pubDate><link>https://news.ycombinator.com/item?id=29343085</link><dc:creator>lkitching</dc:creator><comments>https://news.ycombinator.com/item?id=29343085</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=29343085</guid></item><item><title><![CDATA[New comment by lkitching in "Selecting a programming language can be a form of premature optimization"]]></title><description><![CDATA[
<p>> I said why does it even matter not why can't you do it<p>I've explained why it matters - the types are more precise in my version and if you start from that you can always throw away the extra precision if desired to get to your version. You can't go in the other direction, so starting from your version makes it impossible to safely recover an Int from the returned type of Optional[Int], even if you've already established the precondtion beforehand.<p>> There is 100% casting in your version<p>Creating an Optional[Int] from an Int is a conversion, not a cast. I thought it was obvious from the context but for the avoidance of any doubt, by 'casting' I mean an unsafe narrowing conversion. Optional[Int] is a larger type than Int, so it's trivial to create one from an Int:<p><pre><code>    def pure(x: Int): Optional[Int] = Just(x)
</code></pre>
you clearly can't safely go in the other direction, whether using pattern matching or otherwise. If you disagree, just complete the following definition:<p><pre><code>    def fromOptional(o: Optional[Int]): Int =
        match o with
        | Some(i) => i
        | Nothing => ...
</code></pre>
eventually you need to provide a default value for the case of no value.<p>> Imagine a map with RGB colors as keys.<p>Your example doesn't make sense, what would you expect (lookup Map.empty Red) to return? The optional return value is used to represent the key being missing in the map. Nonetheless the point I was making is that you wouldn't return Nothing from such a function in the event of a precondition failure e.g.<p><pre><code>    def lookup(m: Map[K, V], v: K): Optional[V] =
        if m is None return Nothing
        ...
</code></pre>
you would instead throw an exception if the input map is null and force the caller to handle it. The majority of static type systems are not powerful enough to encode arbitrary properties about values, so you have to decide which ones to check dynamically and which statically. Checking preconditions dynamically is reasonable if encodng them in the type system is too cumbersome.<p>> This prior point involves the creation of the type NonZero[Int] which involves: NonZero.fromInt<p>No, this is not necessarily the only way to create instances of NonZero. You could have a PosNat subtype with members one: PosNat and succ: PosNat -> PosNat. You could have a non-empty list type with a length member.<p>> Every other mathematical operation (+,-,x^y,/,) returns an Int not a NonZero[Int]<p>They don't return Optional[Int] either so I don't see how this is relevant. There's no reason the input has to come from some application of a different operator, it could come from configuration, user input, a property from some other type etc. The question is whether and how to model the constraints in the type. The constraint exists in the argument so it makes sense to constrain the input type, not widen the output.<p>> Notice how the above two sentences are the same?<p>Yes, if all you want to do is avoid establishing the property you care about and silently propagate some information-free 'failure' value to the top level, then you can do it either way. But the entire point of encoding properties in the types is to force you to establish them. These statements highlight the difference:<p>1. I've established the divisor is non-zero, called myDiv, received an Int and continue<p>2. I've established the divisor is non-zero, discarded that information to call yourDiv, recieved an Optional[Int] which cannot be empty, but which must be propagated. You could immediately unwrap the value but now you're just re-creating the dynamic behaviour of a function (Int, Int) -> Int which you've already rejected.</p>
]]></description><pubDate>Wed, 24 Nov 2021 16:08:15 +0000</pubDate><link>https://news.ycombinator.com/item?id=29331385</link><dc:creator>lkitching</dc:creator><comments>https://news.ycombinator.com/item?id=29331385</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=29331385</guid></item><item><title><![CDATA[New comment by lkitching in "Selecting a programming language can be a form of premature optimization"]]></title><description><![CDATA[
<p>> First, Why does this even matter?<p>The reason you can't write my version using yours is that the types are less precise and you can't recover the imprecision in the output type after the fact.
The only safe way to obtain an Int from an Optional[Int] is by providing a default value which doesn't exist in this case.<p>> The Optional[Int] doesn't exist so how do you create it?? You CAST<p>By casting I mean an unchecked narrowing conversion e.g. of the type Optional[Int] -> Int. There's no casting in my version.<p>> if you want to talk about "Well Established" then Optional is more well established<p>This is a false dichotomy, contracts are still used in static languages where you can't or don't want to try represent properties at the type level. You could for example define a function<p><pre><code>    lookup: Map -> Key -> Optional[Value]
</code></pre>
and still add preconditions that the map and key were non-null. The failure to uphold these represent a different kind of 'failure' than the key not being found so it wouldn't make sense to lift them into the return type.<p>> The safe version suffers from your same problem just moved<p>It didn't 'just' move, it moved to the point in the program you actually need to deal with the possibility of a zero divisor i.e. before calling div. Where does the divisor come from in the first place? You seem to be assuming there is necessarily some call to NonZero.fromInt at each call site to div but this is wrong. The non-zeroness of the divisor could be established at some prior point in the program and used in multiple places. In contrast your version has to deal with the possibility of returning None everwhere even if you've already established the property of the divisor beforehand.</p>
]]></description><pubDate>Mon, 22 Nov 2021 16:38:17 +0000</pubDate><link>https://news.ycombinator.com/item?id=29308340</link><dc:creator>lkitching</dc:creator><comments>https://news.ycombinator.com/item?id=29308340</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=29308340</guid></item><item><title><![CDATA[New comment by lkitching in "Selecting a programming language can be a form of premature optimization"]]></title><description><![CDATA[
<p>> This is just your arbitrary preference<p>It's not arbitrary since it's possible to write your function using mine but not vice versa. If you disagree then please implementing the following function without casting:<p><pre><code>    def convertDiv(f: (Int, Int) -> Optional[Int]): (Int, NonZero[Int]) -> Int
</code></pre>
> You're just trying to justify a convention of doing this check before rather than later<p>The convention that callers are responsible for upholding the preconditions of the functions they call is well established: <a href="https://en.wikipedia.org/wiki/Design_by_contract" rel="nofollow">https://en.wikipedia.org/wiki/Design_by_contract</a>. You obviously can't fix precondition violations by checking the result after the fact.<p>> Also Your unsafe version is again worse because it will trigger an exception on zero<p>That is the point of the unsafe version, yes. Sometimes you will statically know the argument is non-zero e.g. NonZero(3). If you want to avoid an exception then use the safe version.</p>
]]></description><pubDate>Sat, 20 Nov 2021 11:39:27 +0000</pubDate><link>https://news.ycombinator.com/item?id=29287203</link><dc:creator>lkitching</dc:creator><comments>https://news.ycombinator.com/item?id=29287203</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=29287203</guid></item></channel></rss>