Exploring the .NET Core Runtime

(mattwarren.org)

251 points | by matthewwarren 1958 days ago

6 comments

  • giancarlostoro 1958 days ago
    C# was the first language after Visual Basic 6 that I learned the most seriously, and after .NET Core I got the most excited about C#. I love that I don't have to reach to Java anymore to go cross platform, or wonder if Mono will support my .NET project or not. In fact, thanks to .NET Core they made .NET Standard so I know if and how my project will run on a modern version of Mono.

    I also love that people are doing write ups about the source of .NET Core and it's internals.

  • bad_user 1958 days ago
    I was never impressed with .NET, I always considered it inferior to the JVM and its ecosystem in terms of runtime capabilities, tooling and open source available, however .NET Core moving to an open source model is super exciting, F# looks cool and I'm happy that Java finally has true competition.

    Also Matt Warren's blog is super cool.

    • lwansbrough 1958 days ago
      Java is not an example of a forward thinking language anymore. It has lagged behind C# in every major feature pretty much since C# arrived in 2000. The JVM has had solid performance improvements since forever, and may still be faster than the CLR, but the JVM still has some pretty glaring shortcomings coming from the .NET world. For example, Java generics (and their underlying implementation in the JVM) suffer from type erasure which makes for an awful developer experience in many scenarios. Java/JVM's story for asynchronous code is also pretty pathetic. This is stuff I don't even think about anymore when writing C# but I groan every time I have to do any "modern" programming in Java.
      • bad_user 1958 days ago
        My opinion is from the other side of the fence ... type erasure is perceived as a weakness of Java, however I believe it is one of JVM's best features, because it didn't cripple its runtime for other languages, this being one of the reasons for why other languages have been flourishing on the JVM (that and the tooling and the open source culture).

        To understand why, we need to talk about 2 separate things:

        ---

        1. Specialization is important for performance and when speaking of .NET, specialization for value types is subsumed in type erasure, but that's not necessarily the case. You can have compiler-driven or even runtime-driven specialization without reification.

        In Scala for example we've had the "@specialize" annotation for a long time, with the compiler being able to specialize generic code just fine. It's not perfect, a better implementation eventually happened in Miniboxing [1] but it withered away due to lack of interest and the ongoing work happening in Dotty / Scala 3 and its newer TASTY distribution format, which should make specialization easier to accomplish.

        Also there is on-going work to bring value types to the JVM and it's happening: http://mail.openjdk.java.net/pipermail/valhalla-spec-experts...

        That said having specialization is pretty cool for fine control of the memory layout and Java developers have to resort to a lot of unsafe hacks for achieving the same thing. But if that cost was paid such that languages like Scala or Haskell could happen on top of the JVM, until they figure out how to do it such that everybody benefits, I think it was a cost worth paying.

        Also consider that the lack of specialization forced the JVM 's engineers to get creative in other areas. For example the JVM has always been great at inlining code at runtime, even for megamorphic call sites. And the new GraalVM has super impressive abilities to eliminate boxing at runtime, which works for dynamic languages too: https://www.graalvm.org/

        ---

        2. Reification is in fact about adding info about type parameters at runtime. This aids in using reflection to make a difference between List<int> and List<string>, but people miss the forest from the trees.

        As a matter of fact such reflection is only needed because languages like Java or C# have very weak static type systems, compared with other languages in the ML family. With an expressive type system, you never need reflection capabilities.

        In Haskell for example the question of whether something is a List<string> or List<int> never, ever happens. In Scala you sometimes need it, but much rarely and you can get it via a compiler-generated `ClassTag`, which is actually a much better approach, because it makes it clear in the signature, this being compile-time reflection. People also like being able to do "new T", however that need completely goes away via proper support for type classes, which both Haskell and Scala have, this being another special purpose band-aid.

        And reification is actually a bad feature to have in the runtime, because it makes it hard for languages to support higher-kinded types, or to build dynamic languages.

        F# does not do higher-kinded types and is less expressive for that reason than OCaml, Scala or Haskell and the primary reason for why it doesn't do higher-kinded types is because it would have to do type erasure by itself, thus forgoing the performance benefits and the interoperability it has with C#.

        Ironically it is support for higher-kinded types in a language that increases its expressive capabilities to the point that you no longer need runtime reflection. In other words ... you're blaming Java for not having a band-aid that happened in C# due to their static type system being basically unsound and thus needing runtime guards and reflection. You quickly get over this once you'll start using a more expressive language ;-)

        [1] http://scala-miniboxing.org/

        • migueldeicaza 1956 days ago
          Reflection is the least of the problems this solves.

          The majority of the issues and gotchas listed in the generics FAQ for Java do not happen in C# at all. It is liberating.

          Reflection is an advanced use case that some people use, but it is rare.

          The lack of flourish in. NET overtime more to what happens when a company takes the reins of a stack and later stops working on it.

          Microsoft for a while invested and designed JavaScript, Python and Ruby versions for .NET. But when conditions changed, there was no interest to keep the projects going. These are all observations from an outsider at the time the projects were defunded.

          Because of the siloed approach to development at the time, and the lack of an external county around those efforts, bootstrapping a community to drive those on their own proved to be very hard. Ruby mostly died, Python is barely surviving.

          The mood in the ecosystem went from "we can build these and speed them up" to "this is an ongoing cost, let us rather interop with the real implementations rather than find constant catch up".

          Meanwhile, languages that Microsoft did not build did flourish, like PHP

          This corporate phenomenon deserves a blog post on its own

          • i_s 1956 days ago
            > The lack of flourish in. NET overtime more to what happens when a company takes the reins of a stack and later stops working on it.

            But couldn't .NET not being a good target for dynamic languages be one of the reasons to stop working on it?

            I keep hearing that .NET is a good platform for other languages, but there doesn't seem to be much empirical evidence for it (at least in dynamic languages).

        • lwansbrough 1958 days ago
          I don’t know about all that in all honesty. I’m not a language designer so I suppose I don’t care about the why. All I know is Java is really bad at fulfilling my generic typing needs. Pragmatism is really important for languages, because without it all you have is a complicated excuse for a bad user experience. It’s sort of like how if you were to optimize English by cutting out redundant letters and combining words instead of making new words, you basically end up with German, which many English speakers would agree is not as nice a language to look at. (Germans may disagree of course because they’re used to it.)
          • bad_user 1958 days ago
            I understand your point, however people complaining about Java are complaining about the language instead of the platform and the platform has evolved to support multiple languages.

            Scala, Kotlin, Clojure, Eta (Haskell), JRuby are the ones I care about and they've been flourishing.

            I'm a pragmatic at heart btw, but pragmatism sometimes leads to ignorance. It's important to see what's available on the other side of the fence without bringing the preconceptions bag with you. Because you can then actually use the metaphorical "best tool for the job".

            It's what I'm doing myself, or at least trying and .NET is without a doubt evolving in a direction I like. And I can also write entire paragraphs of what's better on .NET, I just don't find the generics reification to be one of those things.

        • louthy 1958 days ago
          > As a matter of fact such reflection is only needed because languages like Java or C# have very weak static type systems, compared with other languages in the ML family. With an expressive type system, you never need reflection capabilities.

          The implication here is that you never need runtime type checking, which clearly isn't the case in ML family languages. Sum types for example have an abstract representation that must be resolved at runtime (for pattern matching).

          C# also doesn't need reflection to resolve types (other than through the virtual route - i.e. inherited method resolution), this is a core feature of the CLR though (via `callvirt` [1] if I remember right) and not a weakness of C#'s type-system. C# bakes the concrete generic types into the generated IL.

          [1] https://docs.microsoft.com/en-us/dotnet/api/system.reflectio...

          • bad_user 1957 days ago
            > Sum types for example have an abstract representation that must be resolved at runtime (for pattern matching).

            Indeed, but that's plain tagging, which is basically an int and not the same thing.

            "callvirt" is basically doing a virtual method lookup in a vtable. It's still at runtime, but we are talking about OOP polymorphism now and how late binding works. Languages with type classes don't need that either, but that's a separate discussion and note that I still like OOP.

            > C# also doesn't need reflection to resolve types

            No, the developers do, because the language is not expressive enough to express generic pieces of code without losing type info, so you need reflection for constraints (e.g. "new T" or "instanceOf" checks) and runtime guards for downcasting.

            Classic example: express a "sum" function that works on any Array<A>.

            And note that what F# is doing to solve this particular problem is a hack to workaround both the lack of higher kinded types and of type classes and that does not interoperate with C#, since it requires "inline functions" which are doing type erasure (and it's a big and ugly hack, speaking of ML, because inline functions are no longer values ;-)).

            > C# bakes the concrete generic types into the generated IL.

            Of course, but that too is part of the problem.

            • louthy 1956 days ago
              > No, the developers do, because the language is not expressive enough to express generic pieces of code without losing type info, so you need reflection for constraints (e.g. "new T" or "instanceOf" checks) and runtime guards for downcasting.

              The type system is totally capable of it. The language doesn't make it easy, but the code below is essentially type-classes and class-instances if you squint. It's something the C# team are looking into now anyway, but the main point is the type system doesn't preclude it.

                  public interface Newable<A>
                  {
                      T New();
                  }
              
                  public struct NewableThing : Newable<Thing>
                  {
                      Thing New() => new Thing();
                  }
              
                  public class Foo
                  {
                      public A Bar<NewableA, A>() where NewableA : struct, Newable<A> =>
                          default(NewableA).New();
                  }
              
              > Classic example: express a "sum" function that works on any Array<A>.

                  public interface Num<A>
                  {
                      A Zero();
                      A Add(A x, A y);
                  }
              
                  public struct NumInt : Num<int>
                  {
                      int Zero() => 0;
                      int Add(int x, int y) => x + y;
                  }
              
                  public struct NumFloat : Num<float>
                  {
                      float Zero() => 0;
                      float Add(float x, float y) => x + y;
                  }
              
                  public static A Sum<NumA, A>(A[] xs) where NumA : struct, Num<A> =>
                      xs.Fold(default(NumA).Zero(), (s, x) => default(NumA).Add(s, x));
              
                  int sumInt = Sum<NumInt, int>(new [] {1,2,3,4});
                  int sumFlt = Sum<NumFloat, float>(new [] {1.0,2.0,3.0,4.0});
              
              
              > Of course, but that too is part of the problem.

              That's a very hand wavey statement. It's the goal of all staticly typed compilers to resolve to concrete types at compile time. How is part of any problem? The CLR has the capability to generate new concrete types from generic types at runtime if required, but obviously tries to do as much of that work up front at compile time.

        • jdoe2018 1956 days ago
          You seem to mix up C# and CLR. The CLR can do type erasure if you want. It has an entire runtime called DLR to handle this stuff. Did you even know that? Type erasure was the only way before C# 2.0 when generics where introduced. The CLR can run Java just fine, we used to have J# before Oracle killed it through legal means. It has nothing to do with CLR technical limitations. You can run any fully fledged dynamic language on the CLR such as Python. Where is the practical problem here?

          Next, you can't say that C# has a "weak" type system. It is mainly a strongly and statically typed by design. There are elements of weak typing in C# such as the dynamic keyword, and all C# type safety features can be bypassed if desired. It seems you prefer "weak" to mean "poor" but that is your definition, not the generally accepted one.

          Further, it is completely false to indicate something to the effect that C#/CLR can't let other languages flourish on the platform. The CLR stands for "Common Language Runtime" and can run any language you can imagine, including weakly typed languages. It is a plain old finite stack machine at heart. Unlike JVM, the CLR is an ECMA standard and anyone can implement it and .NET sources are not GPL licensed.

          Scala used to be available on CLR, and the reason it is not supported is more political than technical. You are again trying to make a technical justification for a political decision. Besides, Scala still only runs on JDK 8 which is funny because Oracle JDK will need a commercial license from 2019 to get patches so you're forced to switch to OpenJDK or pay for a Scala runtime.

          The Java community has talked about value types forever, we know that. Good luck unwinding that decision, it is not that easy.

          The cost for unsafe hacks to let Scala and Haskell work had more to do about the fact that .NET was not cross platform at that time. That was not the ecosystem impetus for for having Scala run on JVM. It was more because Java as a language is aging and lacks innovation which is well recognized. The cross platform argument against the CLR is gone now, so there is little reason why we shouldn't be able to run Scala and Haskell on CLR instead if you are after those languages.

          Also, the point about "forcing JVM engineers to get creative" is not really a solid argument at all. They just have to work around the mistakes they made in the past (no value types + runtime type erasure). We also know for a fact that escape analysis in JVM doesn't really cover that many cases anyway in practice. You need to look at the generated code to tell if it works or not. Besides, it is not something that the CLR cannot implement. On the contrary, there is active effort in CLR towards Object Stack Allocation (you can Google for it).

          The general point that reflection is "only" needed because of a "weak static type system" is also not true. You seem to confuse the shape of the API for getting runtime type information with the need for getting such information in the first place. You can try to build a polymorphic serializer in C++ which also has run time type erasure, it is not as easy as in C#. Eventually we need to carry over type information anyway, no matter what language.

          Reflection is not about "adding info about type parameters at runtime". It is much more than that. First, we don't "add information" at runtime for existing types, we can inquire about it. This is super useful for many scenarios. Second, we can generate code at runtime as well with reflection, that is, MSIL op codes. This is also super useful.

          Your use case of "express sum function over Array<A>" is not the direction C# is heading. We have LINQ for the expressiveness part which Java doesn't have apart from some lookalike hacks. Also, for performance, your sum function is going to run at glacier speed compared with .NET Core 3.0 where we will have SIMD instructions and we can execute dedicated AVX code paths depending on memory alignment, in managed code. This stuff used to be C++ only until now. I don't think your Scala and Haskell implementations stand a chance to perform close to that. And reusing a sum function over a read only span is a matter of a one line call. I like the pureness argument but in practice we need to consider performance as well.

    • matthewwarren 1958 days ago
      > Also Matt Warren's blog is super cool.

      Thanks! I'm glad you like it

      • bad_user 1958 days ago
        Have been following you for some time, keep up the good work :-)
  • eksemplar 1958 days ago
    Do people really use .NET core, and if so, why?

    We’ve been a C# house for several years, decades really, and I’ve always preferred it to JAVA so I’m actually excited for Core.

    But we rarely use it. Not because it’s not great, rather because we’re more productive with flask or Django. For Core to really make sense for us, it’d would have to stop being so damn low level, but I guess that maybe it can’t without sacrificing too much efficiency. More importantly it needs better libraries for things that aren’t “built-in”.

    I can certainly see why .NET developers welcome it, because they finally have good cross platform ability. At least until they need to do authentication on a non-standard SAML token, that though easily supported by ADFS but is a bitch in any .NET setup.

    I know we aren’t most use cases, being the public sector and running a gazillion different tech stacks at once, but .NET has never played well once you stepped outside it’s comfortzone and it’s always been so low level that writing library extensions were a bitch. And that may have worked out, so far, but I just don’t see why people stick with it when there are more productive alternatives.

    I say productive, because I don’t think .NET core is lacking technically, but delivering solutions on time and with minimum maintenance requirements afterward is just easier in python or JAVA and I’d imagine others as well.

    But maybe I’m missing something?

    • romanovcode 1958 days ago
      Using .NET Core on production here.

      > but delivering solutions on time and with minimum maintenance requirements afterward is just easier in python or JAVA

      I would agree if you would replace Python/Java with Node, but with these languages I don't see it.

      > on time and with minimum maintenance requirements afterward

      This is my Dockerfile, it is serving me without changes (technically replacing 2.1 with 2.2 is a change) for more than 6 months.

          # Build assets
          FROM node:8-alpine AS assets
          WORKDIR /app
          COPY src/Website/Package.json ./Website/package.json
          COPY src/Website/Gulpfile.js ./Website/
          COPY src/Website/Static/. ./Website/Static
          WORKDIR /app/Website
          RUN npm install
          RUN npm run build
      
          # Build project
          FROM microsoft/dotnet:2.2-sdk-alpine AS build
          WORKDIR /app
          COPY src/Website/*.csproj ./Website/
          WORKDIR /app/Website
          RUN dotnet restore
          WORKDIR /app
          COPY src/Website/. ./Website/
          WORKDIR /app/Website
          RUN dotnet publish -c Release -o dist
      
          # Run project
          FROM microsoft/dotnet:2.2-aspnetcore-runtime-alpine AS runtime
          WORKDIR /app
          COPY --from=build /app/Website/dist ./
          COPY --from=assets /app/Website/Static/dist ./Static/dist
          RUN rm /app/Data/Log/.gitkeep
      
          ENTRYPOINT ["dotnet", "Website.dll"]
      
      
      >But maybe I’m missing something?

      I think you might be.

    • manigandham 1958 days ago
      What exactly do you mean by low-level? Can you give a clear example?

      .NET is an ecosystem of languages, runtimes, and tooling with Visual Studio being regarded by many as the best IDE. Add in the vast MS suite of Windows, Office, Azure, SQL Server, and the rest and you have one of the most productive frameworks to build anything for desktop, mobile, games, server backends, and even databases.

      We moved over to .NET Core since v1.0 to consolidate on Linux/K8S and it's been a great experience. ASP.NET Core is also a very good framework for building webapps, SPAs and APIs that has more functionality in the box than pretty much anything else we've seen.

      Nuget is turning into a solid package management system and there are great 3rd-party projects like IdentityServer for authentication, have you looked at that?

    • kungito 1958 days ago
      I've worked in many languages including C#, Python,Javascript, Typescript, Haskell and while I understand Python and Javascript are amazing for prototyping, I don't see the advantage in the "on time with minimum maintenance" which Python has against C#.

      Could you please expand on this one?

    • jf- 1957 days ago
      As many other sibling comments have asked, can you give examples of what you mean? What issues have you had with finding/writing libraries, what tech stacks have you encountered .NET not playing well with, and exactly what language or framework features are "too low level"? As it is your post is too lacking in details for me to understand where you're coming from.

      I can however offer a simple possible explanation for your experience: you are more proficient in other languages and frameworks because you know them better.

    • outadoc 1958 days ago
      We're using .NET Core, but not really. The tooling is much better than what existed before, so we use it, but still compile against the full net471 framework - for now.
    • scarface74 1957 days ago
      Do people really use .NET core, and if so, why?

      Yet another case of “Do people still watch TV? I haven’t owned a TV in 10 years”, style myopia.

    • pepper_sauce 1958 days ago
      What's your definition of "low level"? You keep using that word, I do not think it means what you think it means
      • eksemplar 1957 days ago
        It’s having to explicitly tell the computer what you want. C# is obviously not C, but it’s not python either.

        By the time we have an app running in Django, we’re not even finished with Entity modelling in a Core web-api.

        I do think stuff like Blazor.Net is promising, but we’re not a technology company, we support thousands of employees who only care about how digitisation can make their lives easier as fast and as stable as possible.

        .NET isn’t the best at that, at least not for us.

        Dont get me wrong, I don’t dislike .NET Core, it think it’s great, I just don’t see how it benefits me.

        • BjorksEgo 1957 days ago
          >Do people really use .NET core, and if so, why?

          >.NET isn’t the best at that, at least not for us.

          You're comparing apples and oranges, djanjo is a web framework, for creating websites, .Net core is a cross platform compiler and the standard library that goes with the C# programming language, which includes some stuff for creating websites (along with desktop, CLI, services etc). Django might work best for you but I want to create a bunch of micro services I'm not going to use it, am I? Not to mention Django works well as a general purpose solution for general purpose websites, If you're building anysort of heavyweight, enterprises level web architecture you're going to want a hell of a lot more control that what django provides for you.

          • eksemplar 1957 days ago
            I specifically said python, flask and Django, the guy I was replying to then asked for an example, where I used only Django, and now you’re using that against me?

            Obviously we don’t use Django for everything. But like with web-applications, a python script or a flask application is always more productive for us than .NET.

            • BjorksEgo 1957 days ago
              I don't disagree that django and flask are good general purpose web app frameworks, but theres a mile gap between "Not the best for general purpose web apps" and "not good for anything", which is why I highlighted the following question

              >Do people really use .NET core, and if so, why?

    • IneffablePigeon 1958 days ago
      I've never used flask and I haven't used Django for a long time, but I'm not sure what you mean by C# being too "low level". What higher level features is it missing that Java or python provide?

      I find I'm most productive in C#, but probably mostly because it's what I use every day and I like the tooling.

    • martijn_himself 1958 days ago
      I have no familiarity with Java but I'm not surprised you are more productive with Python, Flask and Django (although you are conflating languages and frameworks to some extent here) versus C# and .NET Core's way of building API's and web applications.

      I have been developing in C# for years and I still prefer Python's simplicity over C# and .NET. C# and .NET just seems to have too many concepts, indirection, and 'enterpriseyness' for my liking. This is alleviated by things like LINQ, but still an issue for me.

    • quickthrower2 1958 days ago
      I doubt python or Java are easier than .NET it’s just a case of what you are used to. I’ve tried Ruby on Rails and find .NET easier. Probably because of experience.
    • shapiro92 1958 days ago
      i know a few companies using .net core for example checkout.com could you elaborate a bit more on what stops you from using .net core? I have been writing web apps since version 1.0 and had no issues.
    • UK-Al05 1958 days ago
      What do you mean by low level? I can implement .net core web apps pretty damn fast...

      Plus you get a decent static type system!

  • Traubenfuchs 1958 days ago
    How much of the old CLR is in the new .net core runtime /CoreCLR?

    I once read CLR via C# and it was difficult but amazing. Is all of the knowledge in that book now worthless? What about the IL, is the IL still the same?

    • tybit 1958 days ago
      They forked the CLR, cleaned it up, added cross platform support and open sourced it, so a lot would still be relevant would be my guess. The IL has remained unchanged other than possibly a tweak or two to support newer features like non nullable references.
      • matthewwarren 1958 days ago
        Interesting fact, when they forked the CLR, they actually started with the Silverlight code base. See https://channel9.msdn.com/Blogs/dotnet/NET-Foundations-2015-... (somewhere near the beginning)

        I guess it makes sense. Before .NET Core, Silverlight was the most x-plat version of the Microsoft .NET Runtime(s) (excluding Xamarin/Mono), so it would've been a good starting point.

  • revskill 1958 days ago
    Installing .NET stacks including Runtime, SQL Server is a nightmare to me. When an error occurs during installation process, you got stuck , hopeless and depressed. Hey MS, fix your installation process first.
    • pknopf 1958 days ago
      The build scripts (for coreclr, corehost, corefx) are a mess. I worked on getting the build working for Yocto, and there were so many things done differently all over the place. They need a single team to go through their entire build process, top-to-bottom.

      Don't get me wrong though, I worked to integrate it into Yocto because I love .NET/C#, but the build system needs a lot of love.

    • titanix2 1958 days ago
      Did you try SQL Server in a container? It's way more easy to get up and running than installing it on Windows.

      https://hub.docker.com/r/microsoft/mssql-server

      • revskill 1957 days ago
        No, docker is a cancer to me, Docker has the same problem with installation on Windows now. SO if i use Docker to install MS stack, i have two problems now.
    • taspeotis 1958 days ago
      .NET Desktop/Full Framework’s been fine after some initial missteps when they first bundled it in the OS with Vista.

      .NET Core has self contained deployments so you can bundle it with your app in a .zip.

      For development or continuous integration SQL Server comes in a container now.

      • flukus 1958 days ago
        > .NET Desktop/Full Framework’s been fine after some initial missteps when they first bundled it in the OS with Vista

        They first bundled (1.0) with xp, and every OS since. I don't know why they bothered considering practically every app has had to bundle newer versions with the installer. They could have at least pushed newer versions with windows update.

        • thomasz 1958 days ago
          It was so absurd that only a windiv power play against devdiv can adequately explain it.
    • equasar 1958 days ago
      Never heard about this kind of issues, can you share specific ones you had?
      • lovich 1958 days ago
        seconded. I've never had an installation fail, but I have always installed on windows. I'm curious if its due to installing on other OS's that aren't supported as well, as I am looking to jump stacks and thought .net core would have been a hassle free transition to a new OS at least
        • merb 1958 days ago
          installing .net core on linux is easier than on windows (they create native linux packages, so apt install ... or yum install is enough or docker, whatever they provides so much these days), and macos provides a pkg file, which is just click trough.
    • skc 1958 days ago
      Always been piss easy for me, and I've done it for years now.
    • kyllo 1958 days ago
      What platform/OS? Are you installing from a package manager, from binary, or from source?
      • revskill 1957 days ago
        I used Windows before.
    • bpicolo 1957 days ago
      You can run Postgres with .NET these days no problem.

      http://www.npgsql.org/efcore/

    • voltagex_ 1958 days ago
      Where are you getting stuck? What errors?
      • revskill 1957 days ago
        I don't remember because i don't use MS stack anymore for a long time. Just an alert to fix past issues for everyone that needs it.
        • svick 1956 days ago
          How is an issue you had a long time ago relevant now? And why do you say "fix X first" if you don't even know whether it's still a problem?
  • memsom 1957 days ago
    The "two" books mentioned that were written by Serge Lidin are basically the same book, the latter is just an updated version. So really, there's only one book with two editions.
    • matthewwarren 1957 days ago
      Ah, I didn't realise that, thanks for the info