What are some of its advatanges over JS?

  • ☆ Yσɠƚԋσʂ ☆
    link
    fedilink
    arrow-up
    4
    ·
    4 年前

    My experience is that dynamic typing is largely problematic in imperative languages. The main problem is that the data is mutable, and you pass things around by reference. Even if you knew the shape of the data originally, there’s no way to tell whether it’s been changed elsewhere via side effects. Keeping track of that quickly gets out of hand.

    My team has been using ClojureScript for around half a decade now, and we have a few fairly large front-end apps. We haven’t found lack of static typing to be a problem when maintaining large long living applications.

    What I find to be of highest importance is the ability to reason about parts of the application in isolation, and types don’t provide much help in that regard. When you have shared mutable state, it becomes impossible to track it in your head as application size grows. Knowing the types of the data does not reduce the complexity of understanding how different parts of the application affect its overall state.

    I find that immutability plays a far bigger role than types in addressing this problem. Immutability as the default makes it natural to structure applications using independent components. This indirectly helps with the problem of tracking types in large applications as well. You don’t need to track types across your entire application, and you’re able to do local reasoning within the scope of each component. Meanwhile, you make bigger components by composing smaller ones together, and you only need to know the types at the level of composition which is the public API for the components.

    REPL driven development also plays a big role in the workflow. Any code I write, I evaluate in the REPL straight from the editor. The REPL has the full application state, so I have access to things like database connections, queues, etc. I can even connect to the REPL in production. So, say I’m writing a function to get some data from the database, I’ll write the code, and run it to see exactly the shape of the data that I have. Then I might write a function to transform it, and so on. At each step I know exactly what my data is and what my code is doing.

    I typically care about having a formalism is at component boundaries where the component API lives. Clojure provides a runtime contract system with spec that works pretty well for that. It also makes it easier to encode semantics than types. For example, consider a sort function. The types can tell me that I passed in a collection of a particular type and I got a collection of the same type back. However, what I really want to know is that the collection contains the same elements, and that they’re in order. This is difficult to express using most type systems out there, while trivial to do using spec.

    Another aspect of Clojure is that common data structures are what’s used throughout the language. The vast majority of the code ends up being type agnostic as we’re generally chaining higher order functions together. Things like map, filter, reduce, interpose, don’t really care what collection they’re iterating over. The functions that operate on concrete types are passed in as parameters. This approach pushes the code that cares about the concrete types to a shallow domain specific layer.

    The common argument is that it becomes hard to maintain giant dynamically typed applications with millions of lines of code. However, I find that there is very little value to building monolithic software in the first place. Breaking things up into independent components facilitates code reuse and makes it easier to reason about what each component is doing.

    The smallest reusable component is a function representing a transformation that we wish to apply to the data. When we need to solve a problem we simply have to understand the sequence of transformations and map those to the appropriate functions. The functions capture how the tasks are accomplished, while their composition states what is being accomplished.

    Exact same model should be applied at project level as well in my opinion. The project should be composed of simple components, that each encapsulate how things are being done and the way we combine them expresses what the overall project is doing.

    • DessalinesMA
      link
      fedilink
      arrow-up
      2
      ·
      4 年前

      The common argument is that it becomes hard to maintain giant dynamically typed applications with millions of lines of code. However, I find that there is very little value to building monolithic software in the first place. Breaking things up into independent components facilitates code reuse and makes it easier to reason about what each component is doing.

      This is true, but strictly typing things is useful not just internally in your code, but even for talking to outside services. For example all of the objects lemmy receives from the back-end, are strictly typed in the front end… not only does this just list them so you can see what you’re getting, but you also gain intellisense and auto-complete and all that too.

      Your functional components I imagine also still need a structure to the data they’re receiving (is this a bunch of dogs, or car doors), to be able to work with it. Strict types give you compile-time errors when its the wrong function argument.

      I can’t tell you how many times I’ve been saved by accidentally forgetting a field that was marked as required, at compile time.

      I’m sure repl driven development can help, but its also nice to be able to have my typescript compiler list every error / warning in the project, in an easy to view list, and see code errors / warnings in place.

      • ☆ Yσɠƚԋσʂ ☆
        link
        fedilink
        arrow-up
        2
        ·
        edit-2
        4 年前

        I use Reitit for routing on server side, and it leverages spec to generate APIs, do validation, coercion, and even produces a Swagger test page for the API. Here’s an example of how that looks:

        ["/plus"
          {:get  {:summary    "plus with spec query parameters"
                  :parameters {:query {:x int? :y int?}}
                  :responses  {200 {:body {:total int?}}}
                  :handler    (fn [{{{:keys [x y]} :query} :parameters}]
                                {:status 200
                                 :body   {:total (+ x y)}})}}]
        

        The completion will of course be better with static typing, but you can get surprising amount with a dynamic language as well via static analysis. At the same time, completion isn’t really as much of a problem in a functional language because you’re just using the same set of common functions to transform any kind of data. With OO, each class creates its own ad hoc API in form of its methods. And this makes completion really important since knowing how one class behaves tells you nothing about the next.

        Your functional components I imagine also still need a structure to the data they’re receiving (is this a bunch of dogs, or car doors), to be able to work with it. Strict types give you compile-time errors when its the wrong function argument.

        I find that the code that cares about concrete types tends to live at the edges, while a lot of the code in the application is just routing the data where it needs to go. For example, if I pull some records from the db, the code that sends those records to the client doesn’t care about the content of the payload. It only becomes meaningful when I go to render it in the UI. Unfortunately, static typing can easily introduce unnecessary coupling where the code that shouldn’t care about the types ends up being tied to the specific types it operates on. An analogy here would be having delivery trucks that you can only put specific types of packages into. The truck shouldn’t care about the content of the package it’s delivering.

        With runtime contracts I can decide where the type checks will provide the most value and add them there. Most of the projects I end up working on the specs end up being applied around the API level, and act as a specification for what the component is doing. If the specs pass at the API level, then the code behind the API necessarily has to be doing what’s intended as well.

        And you can use spec driven development along side the REPL driven workflow as seen here, so that works quite similarly to type driven development except that you get live feedback as you’re working. The key advantage is that you can build up the state of the application as you’re working on it incrementally without having to restart it to see changes.

        I find the problem is that most people just haven’t used a decent dynamic language with good tooling around it. Pretty much everybody has experience with languages like Js or Python, and I would much rather use a statically typed language than either of those as well.

        I’d argue that the overall language design plays a far bigger role than the type discipline, and it’s misguided to treat it as the defining feature of a language. In practice, there are great languages using both static and dynamic disciplines and vice versa.

      • Ihavehadenough
        link
        fedilink
        arrow-up
        2
        ·
        edit-2
        4 年前

        what is a strict type?

        Also there’s no reason whatsoever why a dynamically typed language cannot be strongly typed

        but you also gain intellisense and auto-complete and all that too.

        I don’t believe this has anything to do with types at all

        I can’t tell you how many times I’ve been saved by accidentally forgetting a field that was marked as required, at compile time.

        There’s no reason why an implementation of a dynamically typed language cannot check for things like this either, this is just a common misconception brought about by the way some interpreters work.