ContaSystemer AS current stats and thank you the community

#1

Hello, Elm community.

We at ContaSystemer AS reached 20k lines of Elm code. A total is 42k LoC including generated Elm code. Here are some stats for us.

  • We have 74 Elm modules with a total of 20.7k LoC
  • The biggest Elm module has 2.5k LoC
  • The next biggest file has 2k LoC (This file manages the shared state between pages and some common functionality for Elm pages)
  • We have 6 generated Elm modules with a total of 21k LoC
  • The biggest generated Elm module has 7.2k LoC (All the application routes)
  • The next biggest generated Elm module has 5.7k LoC (All the translations)
  • We have 22 direct dependencies and 4 indirect ones
  • We are reusing existing functionality made in JS with custom elements which speed up development process quite a lot
  • We have a custom Elm linter based on elm-syntax

Compilation speed

  • It takes 18s to generate and compile all Elm files with Webpack (4s is spent to run headless Chrome to generate all the app routes)
  • It takes 2.2s to compile all Elm code
  • It takes 9s to compile and minify all Elm code
  • It takes 4.9s to compile all Elm code with elm-stuff removed
  • It takes 11.5s to compile and minify all Elm code with elm-stuff removed

Size

  • Not optimized Elm code size is 1043554 bytes (1.04 Mb)
  • Optimized Elm code size is 974322 bytes (0.97 Mb)
  • Minified not optimized Elm code size is 250504 bytes (250 Kb)
  • Minified optimized Elm code size is 198846 bytes (199 Kb)
  • Gzipped minified not optimized Elm code size is 75267 bytes (75 Kb)
  • Gzipped minified optimized Elm code size is 64549 bytes (64.5 Kb)

Stats for JS files.

  • We have 966 JS files with a total of 95k LoC
  • The biggest JS file is 864 LoC
  • Bundled JS file size is 3459816 bytes (3.46 Mb)
  • Bundled minified JS file size is 1361165 bytes (1.36 Mb)
  • Bundled minified gzipped JS file size is 308909 bytes (309 Kb)

0.18 - 0.19 upgrade path

It was very smooth for us because we prepared in advance. We followed the advice of the core team and didn’t use native code. Well, we had a native code but we abandoned it at an early stage. We used special functions instead of toString so we don’t have to change it at the upgrade stage. We wrote “future” functions to work with dates to have a smooth upgrade (Thank for @justinmimbs date package). The only obstacle in our way was time to wait until all the dependencies are updated. The conclusion is if the future plan is known and the advice of the core team is followed then upgrade is straight forward.

Thank you @evancz, the core team and the community. It’s a pleasure to work with Elm and to be a part of the community.

Appendix

We use a slightly modified version of an outer message a.k.a. shared state, a.k.a Elm taco technique to manage common/shared functionality between pages. It’s really great. Please comment below or add a like if you would like us to share more about it.

29 Likes
#2

Question:

It looks like a little over 50% of your Elm code is generated. What is the generated code used for?

Answer:
We have 6 generated Elm files.

  1. We share enums between backend and frontend. In our case backend defines enums. Backend generates a JSON file and we use it on the frontend. For Elm, we convert the JSON file to an Elm module with generated custom types, decoders, encoders and functions to convert an enum to the enum value (e.g. string or integer)
  2. We have a generated file with environment-dependent constants (e.g. API URL)
  3. In our app, each page has a header with an icon. There is a generated Elm module with a custom type for header icons and a function to get an icon path. It’s just a way to have type-safe header icons.
  4. There is a file with generated backend API endpoints. Backend exposes a JSON file with all the endpoints and we generate an Elm module from that. So we can be in sync with backend in term of API endpoints.
  5. In our app, AngularJS manages the routing. There is a file with generated URLs and page names which we can use from Elm to communicate with AngularJS when we want to change a page.
  6. There is a file with all the translations generated from JSON file so we can get type safety for translations.
1 Like
#3

It looks like a little over 50% of your Elm code is generated. What is the generated code used for?

Edit: akoppela was kind enough to answer this question while it was waiting mod approval which is why the answer is before the question

6 Likes
#4

Why did you decide to build a custom Linter?

1 Like
#5

We use elm-analyse at work but it does not have a way to extend it with custom rules. For company-specific rules, we wrote a simple linter. E.g. we have the rule to disallow usage of regex and force to make a parser instead. Or we disallow exposing for anything when importing a module to enforce developers to use module prefixes for functions (e.g. Html.div, Decode.string, etc…). Or we force every top-level function to have documentation.

3 Likes
#6

I’m not sure what others think, but any reason your not open sourcing this?

1 Like
#7

The same question our manager asked me :wink: We had a talk with @stil4m (the author of elm-analyse) and as I remember a way to extend elm-analyse with custom rules may come in the future release. But if it’s interesting for the community we can open source it. I’ll open a separate thread for that.

2 Likes
#8

How is the logic distributed between the (angular) js application and the elm application? What influenced that distribution the most? And are you planning to change that distribution in the future? (what parts of your application went where and why)

1 Like
#9

Could you please elaborate what do you mean by logic distribution?

#10

With distribution I mean how it is spread/divided/carefully-alotted to parts of the overall codebase. I hope that my question is more clear now!

#11

I am curious about the code generation. Which tool are you using for that?

#12

In short, the distribution is following, all new code is written in Elm and all serious refactoring of existing code is done in Elm as well. Where serious means e.g. new version of the component which requires a “fundamental” rewrite. So we maintain two code bases.

What influenced that distribution the most?

We don’t want to rewrite existing AngularJS application to Elm from scratch. We want to come to that point naturally.

And are you planning to change that distribution in the future?

Now we have pretty heavy communication between AngularJS and Elm. It works very well for us. And we are moving towards Elm in all possible ways.

What parts of your application went where and why?

Routing is done in AngularJS. Because Elm routing would require a significant rewrite. It would be possible to move the Elm routing sometime in the future I think. The rest is in short story below.

We started to integrate Elm when we had big AngularJS code base already. There was a task to create a new AngularJS component with autocompletion functionality. It was done in Elm and wrapped with AngularJS component. So it felt natural for AngularJS application.

first

Then there was a task to create a new page. A page in our app may have any of the following parts:

  • Header
  • Alert boxes for system notifications
  • Tabs
  • Modal dialogues

We created a page manager in Elm which manages all of the page parts and the page program itself only manages the page content. All new pages will be created in Elm from now on. I’ll create a separate thread about page manager. In AngularJS we have components for each part as well. In Elm, it’s more clear and easy though.

second

Then there was a task to reuse some of the existing AngularJS components in Elm. So we don’t have to spend time rewriting them. We created an AngularJS service to do so.

third

This is a current situation, please let me know if I answered your questions and if you have any other questions.

1 Like
#13

We are writing Gulp tasks ourselves because Gulp was used already for other parts of the app. Most of the time the process is the same. Read JSON file, take the information, construct an Elm module and write it to the file.

#14

What do you use to generate the code?

… construct an Elm module …

1 Like
#15

We use JS.

Let’s say we have an enums.json file.

{
    "ContaLanguage": {
        "NO": "NO",
        "EN": "EN"
    },
    "AlertType": {
        "POPUP": "POPUP",
        "ALERTBOX": "ALERTBOX"
    }
}

And we want to generate a custom type, decoder and encoder for each enum. We can write a following function.

function generateEnumElmModule() {
    var content = '';

    // Read and parse JSON file
    var enums = JSON.parse(fs.readFileSync('enums.json').toString());

    // Module definition
    content += 'module Enum exposing (..)\n\n';

    // Imports
    content += 'import Json.Decode as Decode\n';
    content += 'import Json.Encode as Encode\n\n';

    Object.keys(enums).forEach(function(enum) {
        var constructors = Object.keys(enums[enum]);

        // Custom type
        content += `type ${enum} = ${constructors.join(' | ')}\n\n`;

        // Decoder
        content += `decoder${enum} : Decoder.Decoder ${enum}\n`;
        content += `decoder${enum} = Decode.string |> Decode.andThen decoderHelper${enum}\n\n`;
        content += `decoderHelper${enum} : String -> Decode.Decoder ${enum}\n`;
        content += `decoderHelper${enum} enum = case enum of\n`;
        constructors.forEach(function (constructor) {
            content += `    "${enums[enum][constructor]}" -> Decode.succeed ${constructor}\n`;
        });
        content += `    _ -> Decode.fail "Invalid enum"\n\n`;

        // Encoder
        content += `encode${enum} : ${enum} -> Encode.Value\n`;
        content += `encode${enum} = Encode.string << toString${enum}\n\n`;

        // To string
        content += `toString${enum} : ${enum} -> String\n`;
        content += `toString${enum} enum = case enum of\n`;
        constructors.forEach(function (constructor) {
            content += `    ${constructor} -> "${enums[enum][constructor]}"\n`;
        });
    });

    // Write Elm file
    fs.writeFileSync("Enum.elm", content);
}

And when we run generateEnumElmModule function it will generate following Enum.elm file.

module Enum exposing (..)

import Json.Decode as Decode
import Json.Encode as Encode

type ContaLanguage = NO | EN

decoderContaLanguage : Decode.Decoder ContaLanguage
decoderContaLanguage = Decode.string |> Decode.andThen decoderHelperContaLanguage

decoderHelperContaLanguage : String -> Decode.Decoder ContaLanguage
decoderHelperContaLanguage enum = case enum of
    "NO" -> Decode.succeed NO
    "EN" -> Decode.succeed EN
    _ -> Decode.fail "Invalid enum"

encodeContaLanguage : ContaLanguage -> Encode.Value
encodeContaLanguage enum = Encode.string << toStringContaLanguage

toStringContaLanguage : ContaLanguage -> String
toStringContaLanguage enum = case enum of
    NO -> "NO"
    EN -> "EN"

type AlertType = POPUP | ALERTBOX

decoderAlertType : Decode.Decoder AlertType
decoderAlertType = Decode.string |> Decode.andThen decoderHelperAlertType

decoderHelperAlertType : String -> Decode.Decoder AlertType
decoderHelperAlertType enum = case enum of
    "POPUP" -> Decode.succeed POPUP
    "ALERTBOX" -> Decode.succeed ALERTBOX
    _ -> Decode.fail "Invalid enum"

encodeAlertType : AlertType -> Encode.Value
encodeAlertType enum = Encode.string << toStringAlertType

toStringAlertType : AlertType -> String
toStringAlertType enum = case enum of
    POPUP -> "POPUP"
    ALERTBOX -> "ALERTBOX"

P.S. the code maybe does not work because I’ve not tested it but the idea should be clear to understand. Feel free to ask any further question.

2 Likes
closed #16

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.