Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Top-level type declarations #815

Open
Happypig375 opened this issue Dec 3, 2019 · 9 comments
Open

Top-level type declarations #815

Happypig375 opened this issue Dec 3, 2019 · 9 comments

Comments

@Happypig375
Copy link
Contributor

@Happypig375 Happypig375 commented Dec 3, 2019

Top-level type declarations

I propose we add top-level type declarations.

namespace Namespace.Namespace // :)
module Nested = do ()
module Namespace.Module // :)
module Nested = do ()
type Namespace.Interface // :(
abstract Method : unit -> unit
type [<Struct>] Namespace.Structure // :(
member _.Method = ()
type Namespace.Class(parameter) // :(
member _.Method = ()

Large classes that are analogous to large modules with a constructor should receive the same syntactic sugar as modules.

The existing way of approaching this problem in F# is having an extra indentation level spanning almost the entire file.

Pros and Cons

The advantages of making this adjustment to F# are

  1. Consistency
  2. Better use of space

The disadvantages of making this adjustment to F# are none that I can think of.

Extra information

Estimated cost (XS, S, M, L, XL, XXL): S

Related suggestions: None

Affidavit (please submit!)

Please tick this by placing a cross in the box:

  • This is not a question (e.g. like one you might ask on stackoverflow) and I have searched stackoverflow for discussions of this issue
  • I have searched both open and closed suggestions on this site and believe this is not a duplicate
  • This is not something which has obviously "already been decided" in previous versions of F#. If you're questioning a fundamental design decision that has obviously already been taken (e.g. "Make F# untyped") then please don't submit it.

Please tick all that apply:

  • This is not a breaking change to the F# language design
  • I or my company would be willing to help implement and/or test this
@toburger

This comment has been minimized.

Copy link

@toburger toburger commented Dec 9, 2019

I don't see any real advantage. This change leads to a code base where each class has it's own file. This is C#'s style of OOP programming. F# discourages this by design.
In F# a class consists usually of very little code (see reason below). I don't see any advantage of splitting your code into many small files.

Large classes are a code smell. They get hard to reason about, because mutable state could be modified all over the place.
My advice: design your code to have immutable helper functions (in a module) and use those functions in your class code. This results in smaller class code and leads towards the goal of "object programming" as mentioned by Don Syme in his talk F# Code I Love

@abelbraaksma

This comment has been minimized.

Copy link

@abelbraaksma abelbraaksma commented Dec 9, 2019

I don't really see the problem that you are trying to solve, because top-level type declarations are already possible:

image

The only difference seems to be that in your example code, you would like to magically add namespaces. But that is not what the syntax for modules does. It creates nested modules:

module X.Y.Z     // declares namespace X.Y and inside it, module Z
module Foo = ()  // creates module Z.Foo (nested module) inside namespace X.Y

This syntax only works with the "namespace + module" statement at the top of your file. Inside the module you can place let bindings, nested modules and types.

If we were to allow this syntax for types, it should form the same analogy:

type X.Y.Z        // declares namespace X.Y and inside it, type Z
type Foo() = ...  // creates type Z.Foo (nested type) inside namespace X.Y

However, nested types are not allowed, so I think it is reasonable that we don't allow this syntax until nested types are allowed.

You wrote:

type Namespace.Interface // :(
abstract Method : unit -> unit

But this is not analogous. Analogous would be that under Namespace.Interface you could create nested types, just like you can create nested modules. Orthogonality of a language is a great thing, I don't think we should break that.

The existing way of approaching this problem in F# is having an extra indentation level spanning almost the entire file.

Not quite, as I showed you, you can have multiple types at top-level. However, you cannot nest them. And members must be indented to show that they belong to that type. But if you don't like far indentation, you can set it to one space (but I doubt that makes your code more readable).

Perhaps there's something to say for not having to indent members of a type, just like you don't have to indent let-bindings of a module. But I believe common code practice is to always indent what belongs to a certain module, as that aids in readability. Since indenting is structural and fundamental to F#, I think we should keep this clarity.

Moreover, if you have multiple types (common) and multiple modules (less common) in a single file, if you weren't indenting members, the only way to find out what a member belongs to is to scroll back and look very closely at type declarations. Specifically for the case you specify, large classes, this is a pain to readability, and indentation, I think, helps memorize that you are "a level inside something else" while coding. While scrolling, the unindented type or module declarations stand out and help navigate the code.

@Happypig375

This comment has been minimized.

Copy link
Contributor Author

@Happypig375 Happypig375 commented Dec 9, 2019

@toburger
Not all large classes are mutable. Consider this class:

type Translations(language) =
    let case chinese english =
        match language with Language.Chinese -> chinese | Language.English -> English

    member _.``Loading…`` = case "載入中⋯⋯" "Loading…"
    member _.``Network is unstable, please check your Internet connection.`` = case "網路連線不正常請確認" "Network is unstable, please check your Internet connection."
    member _.``Quit`` = case "離開" "Quit"
    member _.``Unable to login automatically, please confirm.`` = case "無法自動登入請確認" "Unable to login automatically, please confirm."
    member _.``Acknowledge`` = case "確認" "Acknowledge"
    member _.``User ID must be between 3 and 20 characters.`` = case "帳號長度必須在3~20個字元之內" "User ID must be between 3 and 20 characters."
    member _.``Password must be between 4 and 12 characters.`` = case "密碼長度必須在4~12個字元之內" "Password must be between 4 and 12 characters."
    member _.``Passwords do not match!`` = case "密碼輸入內容不一致!" "Passwords do not match!"
    member _.``Incorrect date format(s).`` = case "日期輸入格式錯誤" "Incorrect date format(s)."
    member _.``Set successfully!`` = case "設定成功!" "Set successfully!"
    member _.``Timeout!`` = case "設定逾時!" "Timeout!"
    //...

If the module with functions approach is to be used, there would be annoying additional parameter passing and brackets needed whenever the property is called.
Before you say that this can be solved by resource files, I would like to point out that resource files do not provide type safety and cannot guarantee the existence of the resource key.

@cartermp

This comment has been minimized.

Copy link
Member

@cartermp cartermp commented Dec 9, 2019

I share @abelbraaksma's confusion with this point:

The existing way of approaching this problem in F# is having an extra indentation level spanning almost the entire file.

Since as @abelbraaksma points out, you don't need to indent types in a file:

namespace MyNameSpace

type C1() = class end
type C2() = class end
type C3() = class end

Only if you contain them in a module within that file is it necessary to indent.

@Happypig375

This comment has been minimized.

Copy link
Contributor Author

@Happypig375 Happypig375 commented Dec 9, 2019

@cartermp I'm referring to indentation for members.

@toburger

This comment has been minimized.

Copy link

@toburger toburger commented Dec 9, 2019

@toburger
Not all large classes are mutable. Consider this class:

....

If the module with functions approach is to be used, there would be annoying additional parameter passing and brackets needed whenever the property is called.
Before you say that this can be solved by resource files, I would like to point out that resource files do not provide type safety and cannot guarantee the existence of the resource key.

Completely OT: In the specific case of localization I would recommend to stick with the standard approach and use .resx-files (there are lots of tools to facilitate the translation process) in combination with the ResX type provider, which allows you to use .resx-files in a type safe manner.

@abelbraaksma

This comment has been minimized.

Copy link

@abelbraaksma abelbraaksma commented Dec 9, 2019

If the module with functions approach is to be used, there would be annoying additional parameter passing and brackets needed whenever the property is called.

Why? Your code suggests a script that does translations. While I disagree to the approach (best practice is using resx, and the drawbacks can be catered for), assuming the language is set once anyway, you can write the same code in a parens-less way in a module (but I don't know the rest of your code, so take this advice as is):

module Translations =
    let case chinese english =
        match Glob.Language with Language.Chinese -> chinese | Language.English -> English

    // no parens, just evaluated once on first use
    let ``Loading…`` = case "載入中⋯⋯" "Loading…"
    //... 

However, it seems to me that your code is clearer with the indentation either way. I wouldn't remove the indentation for the module either. Indentations are so basic to F#, it just feels unnatural if they're suddenly gone (matter of taste, I know, but after years of F#, I've started feeling this whole indentation stuff as natural).

As an aside, if you do decide to use resx, it generally works easier from a C# project, imo. And you can use tools like these to get better type safety: https://marketplace.visualstudio.com/items?itemName=Paruz.ExtendedStronglyTypedResourceGenerator (may need to recompile it to get it to work in VS 2019).

But I'm digressing. It wasn't clear from your original suggestion that you wanted to treat indentation more lenient for members only, as you were talking about namespaces etc. I don't think that's a good idea per my comments before, and may not even be possible without introducing ambiguities in the parser.

@Happypig375

This comment has been minimized.

Copy link
Contributor Author

@Happypig375 Happypig375 commented Dec 10, 2019

assuming the language is set once anyway

No. There is language-changing in-program.

@abelbraaksma

This comment has been minimized.

Copy link

@abelbraaksma abelbraaksma commented Dec 10, 2019

Well, in that case there are other coding patterns, not in the least the resx approach with type provider mentioned earlier, or an event-based, or global mutable setting for the language paired with unit functions.

Using in-code strings like in your example is hard to maintain (consider adding a language, ouch), but if you have a large code base, it can be equally hard to refactor all of it.

Am I right to a assume that your original request was mainly to make this large type more easily maintainable?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
4 participants
You can’t perform that action at this time.