Register a SA Forums Account here!
JOINING THE SA FORUMS WILL REMOVE THIS BIG AD, THE ANNOYING UNDERLINED ADS, AND STUPID INTERSTITIAL ADS!!!

You can: log in, read the tech support FAQ, or request your lost password. This dumb message (and those ads) will appear on every screen until you register! Get rid of this crap by registering your own SA Forums Account and joining roughly 150,000 Goons, for the one-time price of $9.95! We charge money because it costs us money per month for bills, and since we don't believe in showing ads to our users, we try to make the money back through forum registrations.
 
  • Post
  • Reply
redleader
Aug 18, 2005

Engage according to operational parameters

Xarn posted:

Congratulations, your modern language handles enums even worse than C++ :v:

yeah. i think everyone who uses c# is aware of and disappointed in how shittily enums are handled. i'm personally disappointed the language team hasn't shown any interest in trying to improve them

Adbot
ADBOT LOVES YOU

Dylan16807
May 12, 2010

Kilson posted:

Everyone knows booleans have 3 values: True, False, and FileNotFound

https://docs.microsoft.com/en-us/dotnet/api/microsoft.office.core.msotristate?view=office-pia

OddObserver
Apr 3, 2009

Why does this 3-state bool seem to have 5 values?

ultrafilter
Aug 23, 2007

It's okay if you have any questions.


OddObserver posted:

Why does this 3-state bool seem to have 5 values?

Only two of them are supported, so it's fine.

Deffon
Mar 28, 2010

Really disliked C# enums, it's a half-baked abstraction that's useful for c-api integration, but less useful for writing modern code.

While it's sometimes the best solution, it's not usually a good idea to create an enum with the idea of adding new values over time.
Adding new values is a pain because you have to update all call sites, some of which may be out of your control.

This is where interfaces really shine, all you have to do is create a new implementation, and you are required to implement all operations and they are all in the same place.
On the flip-side, adding a new operation to an interface is hard, and adding a new operation for an enum is easy.

Xarn
Jun 26, 2015

dwazegek posted:


In .NET Framework it doesn't. This used to be a weird edge case, because the switch statement would only match the "true" case if the boolean actually had binary value 0x01, so if you marshalled a non 0x00/0x01 value to a boolean, it wouldn't match any case statements. Which also means that the switch statement is not exhaustive, as it only covers 2 out of the possible 256 values.



Hold up, C# lets you just blast bits into memory? I thought it was supposed to be a modern and safer language, not like C++ :v:

Impotence
Nov 8, 2010
Lipstick Apathy

Xarn posted:

Hold up, C# lets you just blast bits into memory? I thought it was supposed to be a modern and safer language, not like C++ :v:
All of my C# functions begin with [SuppressUnmanagedCodeSecurity] and [UnmanagedFunctionPointer(CallingConvention.Cdecl)] and is wrapped in unsafe{}

Space Gopher
Jul 31, 2006

BLITHERING IDIOT AND HARDCORE DURIAN APOLOGIST. LET ME TELL YOU WHY THIS SHIT DON'T STINK EVEN THOUGH WE ALL KNOW IT DOES BECAUSE I'M SUPER CULTURED.

Xarn posted:

Hold up, C# lets you just blast bits into memory? I thought it was supposed to be a modern and safer language, not like C++ :v:

C# lets you use the "unsafe" language keyword to drop into a context that lets you work with pointers and handle memory outside the runtime's safety fences. It's not entirely unmanaged, but you can use features like the "fixed" keyword to make sure that chunks of memory aren't moved around under the covers by the runtime, or GC'd while you still need them.

"Unsafe code - C# language specification posted:

While practically every pointer type construct in C or C++ has a reference type counterpart in C#, nonetheless, there are situations where access to pointer types becomes a necessity. For example, interfacing with the underlying operating system, accessing a memory-mapped device, or implementing a time-critical algorithm may not be possible or practical without access to pointers. To address this need, C# provides the ability to write unsafe code.

In unsafe code it is possible to declare and operate on pointers, to perform conversions between pointers and integral types, to take the address of variables, and so forth. In a sense, writing unsafe code is much like writing C code within a C# program.

Java effectively has the same thing through the sun.misc.unsafe library, except the pointers are plain old ints/longs, and the behavior is only a de facto standard instead of part of the language itself. There was a bunch of drama about formally deprecating it, because it's been used for bits of performance-critical code in a lot of web server frameworks.

Foxfire_
Nov 8, 2010

C# is willing to sacrifice ideological purity to be a more useful tool. That's not unusual, pretty much every general-purpose systems language makes the same decision. Even Rust lets you do the same thing.

Deffon posted:

Really disliked C# enums, it's a half-baked abstraction that's useful for c-api integration, but less useful for writing modern code.

While it's sometimes the best solution, it's not usually a good idea to create an enum with the idea of adding new values over time.
Adding new values is a pain because you have to update all call sites, some of which may be out of your control.

This is where interfaces really shine, all you have to do is create a new implementation, and you are required to implement all operations and they are all in the same place.
On the flip-side, adding a new operation to an interface is hard, and adding a new operation for an enum is easy.

:confused: enums and interfaces are completly orthogonal concepts that don't have anything to do with each other

Deffon
Mar 28, 2010

Foxfire_ posted:

:confused: enums and interfaces are completly orthogonal concepts that don't have anything to do with each other

If you know all the subclasses/enum instances and operations that you could ever want for your type at the start you could use either or.
You still have to define your operations for the enums as free-standing functions, but given that you could e.g. define a shape hierarchy using either.
To attach state, you also need unsafe casting in the case of C#, but more functional languages solve it with sum types.

I was just pointing it out that the issues of updating switches in languages like C# could be avoided by using interfaces, with other potential drawbacks since I don't know the details of this particular case.

Deffon fucked around with this message at 21:46 on Feb 13, 2021

Plorkyeran
Mar 22, 2007

To Escape The Shackles Of The Old Forums, We Must Reject The Tribal Negativity He Endorsed

Foxfire_ posted:

C# is willing to sacrifice ideological purity to be a more useful tool. That's not unusual, pretty much every general-purpose systems language makes the same decision. Even Rust lets you do the same thing.


:confused: enums and interfaces are completly orthogonal concepts that don't have anything to do with each other

ADTs and Interfaces are very similar concepts that make opposite trade offs, and while C# enums are lame and not ADTs they’re an important piece of creating one in C#.

Anything which does not involve external extensibility which uses an interface could be refactored to use an enum instead.

Foxfire_
Nov 8, 2010

I guess you could try to shove all the logic that a thing uses into some interface'd thing, but it seems to me like it'd be usually be a mess and more confusing than just doing the simple thing. 99% of enums are going to be things like:

- This serial receiver has a state that is one of IDLE, RX_NORMAL, or RX_ESCAPE_BYTE
- This window is either MINIMIZED, MAXIMIZED, FLOATING, or FULLSCREEN
- The game's difficulty is one of EASY, NORMAL, or HARD

Doing something like having WindowState be an interface with functions for every operation you're going to ever do that cares about a window is going to be grouping logic from lots of unrelated things and make reading any particular thing a lot harder.

e:
I guess kind of are you more likely to be asking 'what does this particular thing do for various enumerated values?' vs 'how do the various enumerations change the entire universe of stuff?'. I could see the second being useful occasionally, like you might want to see all the code that changes in a game by difficulty in one place, but usually the first seems more likely.

Foxfire_ fucked around with this message at 00:19 on Feb 14, 2021

Plorkyeran
Mar 22, 2007

To Escape The Shackles Of The Old Forums, We Must Reject The Tribal Negativity He Endorsed
That's the opposite set of tradeoffs coming into play. ADTs make it easy to define new operations on the set of types but make it hard to add new types, while Interfaces make it easy to add new types but hard to add operations to the set of types.

The "expression problem" is trying to come up with a way to make both easy.

Xarn
Jun 26, 2015

Foxfire_ posted:

C# is willing to sacrifice ideological purity to be a more useful tool. That's not unusual, pretty much every general-purpose systems language makes the same decision. Even Rust lets you do the same thing.



And if the safe parts of your language then have to safeguard against bool having 256 potential states/representations, you are doing it wrong.

dwazegek
Feb 11, 2005

WE CAN USE THIS :byodood:

Xarn posted:

And if the safe parts of your language then have to safeguard against bool having 256 potential states/representations, you are doing it wrong.

It's only if you switch on a boolean, which no one does because why would you. I guess it could also still be an issue if you use unsafe code to read the bits of a boolean and only expect 0x00 or 0x01, but then you're really doing some stupid stuff.

Using it in as part of a boolean expression, or in an if/while/for/ternary/whatever statement, will work correctly. Even a direct comparison to another boolean works as it should (i.e. true == true, regardless of whatever bits the true value has). Also, as I mentioned, it seems to have been fixed in later compiler or language versions.

It really seems to have been a bug and not a flaw in the actual language.

xtal
Jan 9, 2011

by Fluffdaddy
I would expect switching on a boolean to compile to the exact same code as an if statement

Bruegels Fuckbooks
Sep 14, 2004

Now, listen - I know the two of you are very different from each other in a lot of ways, but you have to understand that as far as Grandpa's concerned, you're both pieces of shit! Yeah. I can prove it mathematically.

Xarn posted:

And if the safe parts of your language then have to safeguard against bool having 256 potential states/representations, you are doing it wrong.

or you could be vb, where booleans are 16-bit wides, true = -1, false = 0, casting any other value to boolean means true. if you've ever wondered why variant_bool = -1 when doing windows programming, that's why.

pokeyman
Nov 26, 2006

That elephant ate my entire platoon.
Objective-C used signed char as its boolean type for awhile, so you could gently caress yourself over by checking if (x == YES) because YES was just 1. (if (x) works ok.) You'd also get people taking sorta reasonable shortcuts like returning a collection's count as a boolean to indicate "false when empty", which is how you might end up with non-0 or 1 as a boolean value surprisingly often,

Bruegels Fuckbooks posted:

or you could be vb, where booleans are 16-bit wides, true = -1, false = 0, casting any other value to boolean means true. if you've ever wondered why variant_bool = -1 when doing windows programming, that's why.

If -1 is represented as 0xFFFF then this makes some kind of sense, lotta ones in that representation.

brap
Aug 23, 2004

Grimey Drawer

xtal posted:

I would expect switching on a boolean to compile to the exact same code as an if statement

It’s easy to go to sharplab and verify this: https://sharplab.io/#v2:CYLg1APgAgT...g5OBgC+lrlI2UA=

Volte
Oct 4, 2004

woosh woosh

pokeyman posted:

If -1 is represented as 0xFFFF then this makes some kind of sense, lotta ones in that representation.
Yeah, it's because legacy VB didn't even have short-circuiting logical operators, only bitwise operators. Not False = True was equivalent to ~0 == -1 in C.

Zopotantor
Feb 24, 2013

...und ist er drin dann lassen wir ihn niemals wieder raus...

Bruegels Fuckbooks posted:

or you could be vb, where booleans are 16-bit wides, true = -1, false = 0, casting any other value to boolean means true. if you've ever wondered why variant_bool = -1 when doing windows programming, that's why.

True being -1 goes back to at least the original Microsoft BASIC, possibly even farther.
(PET BASIC is the only Microsoft product I ever voluntarily used.)

Foxfire_
Nov 8, 2010

Xarn posted:

And if the safe parts of your language then have to safeguard against bool having 256 potential states/representations, you are doing it wrong.
That's impossible to avoid. If you could algorithmically prove that some unsafe construct didn't break any other state and always produced valid bit patterns in everything it touched, it wouldn't need to be an unsafe construct to begin with.

It's also kind of a separate issue. C# enums could have been defined so that it wasn't permitted to have an unlisted integer value in one (with normal code doing runtime checks to enforce that). Then a switch wouldn't need paths for handling unlisted values since the runtime could assume they never happened. If you used unsafe code to make one anyway, you'd crash the runtime, but that's not different from all the other ways you can use unsafe code to violate assumptions and crash the runtime (put random garbage in a pointer/reference).

Boolwise, C++ is still worse since it doesn't prescribe any particular bit representation. It mandates the conversions, (true->1, false->0, 0->false, nonzero->true), but not what bits are actually stored in the bool. If you memcpy() to or from a bool, you technically have no portable expectation for what they mean. memcpy()-ing stuff in could also plausibly break real compilers if they implement an equality test by subtracting and then testing a zero flag.

Tei
Feb 19, 2011

That 0 is false and !=0 is true may look like a hack to programmers that live in high level languages, but make generally everything so much simpler that it ascend to the level of Holy.

Is really useful. It conducts to code that is easy to write and easy to read, instead of a novelization of a Tolkien movie but with objects.

Xarn
Jun 26, 2015

Foxfire_ posted:

That's impossible to avoid. If you could algorithmically prove that some unsafe construct didn't break any other state and always produced valid bit patterns in everything it touched, it wouldn't need to be an unsafe construct to begin with.

Boolwise, C++ is still worse since it doesn't prescribe any particular bit representation. It mandates the conversions, (true->1, false->0, 0->false, nonzero->true), but not what bits are actually stored in the bool. If you memcpy() to or from a bool, you technically have no portable expectation for what they mean. memcpy()-ing stuff in could also plausibly break real compilers if they implement an equality test by subtracting and then testing a zero flag.

Right, but you should minimize how much the unsafe parts can infect you back, or at least how much you safeguard against that. Take Rust as an example: the compiler does basically no checks on unsafe blocks, and if your unsafe block messed something up terribly, that's your own drat problem. It does not try adding code to maybe catch some of those mess ups.

LOOK I AM A TURTLE
May 22, 2003

"I'm actually a tortoise."
Grimey Drawer
https://github.com/angular/vscode-ng-language-service/issues/1112

Ignore the actual GitHub issue. Look at the font the guy is using in his editor. Absolute barbarian.

Sagacity
May 2, 2003
Hopefully my epitaph will be funnier than my custom title.

LOOK I AM A TURTLE posted:

Look at the font the guy is using in his editor
It's Ligature Fonts: Endgame

Presto
Nov 22, 2002

Keep calm and Harry on.

Tei posted:

That 0 is false and !=0 is true may look like a hack to programmers that live in high level languages, but make generally everything so much simpler that it ascend to the level of Holy.

Is really useful. It conducts to code that is easy to write and easy to read, instead of a novelization of a Tolkien movie but with objects.

It maps to how most CPUs work. Want to see if something is zero? Load it into a register and check the Z bit in the status register. Super fast.

DELETE CASCADE
Oct 25, 2017

i haven't washed my penis since i jerked it to a phtotograph of george w. bush in 2003
it maps to twitter also. you can make up anything you want and pass it off as true! unless you happen to pick the one thing that is false (that day)

Tei
Feb 19, 2011

DELETE CASCADE posted:

it maps to twitter also. you can make up anything you want and pass it off as true! unless you happen to pick the one thing that is false (that day)

this post also evaluate to true!

Munkeymon
Aug 14, 2003

Motherfucker's got an
armor-piercing crowbar! Rigoddamndicu𝜆ous.



redleader posted:

yeah. i think everyone who uses c# is aware of and disappointed in how shittily enums are handled. i'm personally disappointed the language team hasn't shown any interest in trying to improve them

Simply roll your own https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/enumeration-classes-over-enum-types

... and you can't use them in a switch anmore, solving two problems :angel:

Hammerite
Mar 9, 2007

And you don't remember what I said here, either, but it was pompous and stupid.
Jade Ear Joe

is this one of those things where it's a contest to see how many problems you can find with the linked code?

Munkeymon
Aug 14, 2003

Motherfucker's got an
armor-piercing crowbar! Rigoddamndicu𝜆ous.



I don't think it's meant as a complete example, but it's also not the one I was trying to find that uses the curiously recurring template pattern instead of IComparable

LOOK I AM A TURTLE
May 22, 2003

"I'm actually a tortoise."
Grimey Drawer
I've been using this enum pattern recently in C# (I typed this up from memory so it probably has errors):

C# code:
// definition
public abstract class FooResult
{
    public static readonly FooNotFoundResult NotFound { get; } = new FooNotFoundResult();
    public static readonly FooAccessDeniedResult AccessDenied { get; } = new FooAccessDeniedResult();

    public static SuccessfulFooResult Success(Foo foo) => new SuccessfulFooResult(foo);
}

public class FooNotFoundResult : FooResult
{
}

public class FooAccessDeniedResult : FooResult
{
}

public class SuccessfulFooResult : FooResult
{
    public SuccessfulFooResult(Foo foo) => Foo = foo;

    public Foo Foo { get; }
}

// usage
public class Producer // e.g. a repository
{
    public FooResult GetFoo(int id)
    {
        var foo = GetFooById(id);
        if (foo == null) return FooResult.NotFound;
        else if (!HasAccess(foo)) return FooResult.AccessDenied;
        else return FooResult.Success(foo);
    }
}

public class Consumer // e.g. an API controller
{
    public IActionResult HandleFoo(int id)
    {
        return _producer.GetFoo(id) switch
        {
            FooNotFoundResult _ => NotFound(),
            FooAccessDeniedResult _ => Forbid(),
            SuccessfulFooResult foo => Ok(foo.Foo),
            _ => throw new InvalidOperationException("Unexpected foo result"),
        };
    }
}
Pros:
- The code that produces results looks very similar to regular enum code.
- You can use the switch expression in the code that consumes results.
- You can attach extra data to each enum value, like with actual union/sum types.
- It's easy to change the return data associated with each output type, without changing a lot of function signatures. A good example would be adding an error type to the AccessDenied value.

Cons:
- The code where you define the enum is very verbose.
- Unlike regular enums, the type names leak into the global namespace.
- They're also less discoverable than with enum values, since they're just classes.
- You still can't guarantee exhaustive pattern matches at compile time.
- (edit: the performance is probably pretty bad)

I'm a little torn on the pattern, but overall I prefer it to standard enums. Hopefully C# just gets real sum types at some point.

LOOK I AM A TURTLE fucked around with this message at 11:10 on Feb 20, 2021

dwazegek
Feb 11, 2005

WE CAN USE THIS :byodood:
You could define the derived types as nested types of the base class, that would make the derived types a bit more discoverable, although then you'd get name clashes with the properties.

Making the base class' ctor protected internal or even private protected would prevent types from being added outside the assembly. Still no guarantee that the switch statements are exhaustive, but with regular enums they aren't either...

Another downside is that, since it's a class, there is no compile time way to prevent null values. The newer non-nullable reference type stuff helps, but it offers no guarantees.

Ola
Jul 19, 2004

You can do that with F# while you wait for C# to support it properly. It's so easy to refactor and maintain as well. And the match expressions are dreamy.

code:
match thirdPartyApiResult with
| Ok data -> Success (mySpecialParser data)
| Error e [ when e.StatusCode = 401 ] -> ErrorMessage "wrong password"
| Error e [ when e.StatusCode > 401 and e.StatusCode < 500 ] -> ErrorMessage "you hosed up"
| Error e [ when e.StatusCode >= 500] -> ErrorMessage "they hosed up"
| Error e  -> ErrorMessage "we hosed up"

LOOK I AM A TURTLE
May 22, 2003

"I'm actually a tortoise."
Grimey Drawer

dwazegek posted:

You could define the derived types as nested types of the base class, that would make the derived types a bit more discoverable, although then you'd get name clashes with the properties.

Making the base class' ctor protected internal or even private protected would prevent types from being added outside the assembly. Still no guarantee that the switch statements are exhaustive, but with regular enums they aren't either...

Another downside is that, since it's a class, there is no compile time way to prevent null values. The newer non-nullable reference type stuff helps, but it offers no guarantees.

1. Yeah, I've considered using nested classes. It solves not only the discoverability problem but also the global namespace leak problem. The downside is that the names get very long.

2,. I do make the abstract class constructor protected, yeah. In my actual code I also have a generic Result<T> that I reuse for different result types, since the NotFound/AccessDenied/Success pattern comes up a lot for me, which helps with the boilerplate problem on the definition side.

3. NNRTs are indeed an important part of the puzzle. They don't give an ironclad guarantee, but the level of safety they give is no worse than what you already have with .NET style enums (arguably better). I probably wouldn't use this pattern without NNRTs enabled.

Ola posted:

You can do that with F# while you wait for C# to support it properly. It's so easy to refactor and maintain as well. And the match expressions are dreamy.

code:
match thirdPartyApiResult with
| Ok data -> Success (mySpecialParser data)
| Error e [ when e.StatusCode = 401 ] -> ErrorMessage "wrong password"
| Error e [ when e.StatusCode > 401 and e.StatusCode < 500 ] -> ErrorMessage "you hosed up"
| Error e [ when e.StatusCode >= 500] -> ErrorMessage "they hosed up"
| Error e  -> ErrorMessage "we hosed up"

That's neat, yeah. C# actually also has fairly powerful pattern matching now, but the missing ingredient is proper algebraic types.

dwazegek
Feb 11, 2005

WE CAN USE THIS :byodood:

Ola posted:

You can do that with F# while you wait for C# to support it properly. It's so easy to refactor and maintain as well. And the match expressions are dreamy.

code:
match thirdPartyApiResult with
| Ok data -> Success (mySpecialParser data)
| Error e [ when e.StatusCode = 401 ] -> ErrorMessage "wrong password"
| Error e [ when e.StatusCode > 401 and e.StatusCode < 500 ] -> ErrorMessage "you hosed up"
| Error e [ when e.StatusCode >= 500] -> ErrorMessage "they hosed up"
| Error e  -> ErrorMessage "we hosed up"

AFAIK you can do all of this in C# 9, with a very similar syntax.

Edit: C# 9 isn't officially supported on anything other than Net5, but, unofficially, if you enable langversion latest you can use some features, including those patten matching features on earlier versions as well, including even older framework versions as far back as at least 4.7.2.

dwazegek fucked around with this message at 11:54 on Feb 21, 2021

LOOK I AM A TURTLE
May 22, 2003

"I'm actually a tortoise."
Grimey Drawer

dwazegek posted:

You could define the derived types as nested types of the base class, that would make the derived types a bit more discoverable, although then you'd get name clashes with the properties.

Making the base class' ctor protected internal or even private protected would prevent types from being added outside the assembly. Still no guarantee that the switch statements are exhaustive, but with regular enums they aren't either...

Another downside is that, since it's a class, there is no compile time way to prevent null values. The newer non-nullable reference type stuff helps, but it offers no guarantees.

I thought about this some more, and I think using nested classes actually solves most of the problems with the pattern. As you say it solves the issues with discoverability, and also the namespace leak issue. The name clashes between the types and the properties aren't a big deal, since it often makes decent sense to have a suffix on the end of the type names like in my example.

When the classes are nested you can actually use a completely private constructor on the base class, since nested classes can access private stuff on the containing class. That way you can make it impossible to create other subclasses without resorting to runtime reflection tricks. And the best part is that if you want to you can even avoid having to type out FooResult.FooNotFoundResult in the consumer code if you use using static FooResult.

Now I'm thinking the only downsides of the class enum pattern are the verbosity at the definition site and the performance hit.

I know the pattern has been known for a long time (e.g. https://codeblog.jonskeet.uk/2014/10/23/violating-the-smart-enum-pattern-in-c/), but I think the value proposition got a lot stronger when pattern matching entered the picture.

Hammerite
Mar 9, 2007

And you don't remember what I said here, either, but it was pompous and stupid.
Jade Ear Joe

LOOK I AM A TURTLE posted:

I know the pattern has been known for a long time (e.g. https://codeblog.jonskeet.uk/2014/10/23/violating-the-smart-enum-pattern-in-c/), but I think the value proposition got a lot stronger when pattern matching entered the picture.

On that page:

quote:

The class is not sealed, but it prevents other classes from subclassing it by using a private constructor.

Why? Skeet doesn't explain why it's done this way instead of by sealing it. Presumably he thinks this is obvious, but it isn't to me.

e: On looking at it a second time, the class contains nested private subclasses of itself. When I glanced at the code for the first time, I took the nested classes to be methods.

Hammerite fucked around with this message at 14:42 on Feb 21, 2021

Adbot
ADBOT LOVES YOU

NihilCredo
Jun 6, 2011

iram omni possibili modo preme:
plus una illa te diffamabit, quam multæ virtutes commendabunt

LOOK I AM A TURTLE posted:

And the best part is that if you want to you can even avoid having to type out FooResult.FooNotFoundResult in the consumer code if you use using static FooResult.

I have used this pattern and I would just avoid repeating the base class words from the subclass name. So its full name would be FooResult.NotFound, much like an enum in fact.

  • 1
  • 2
  • 3
  • 4
  • 5
  • Post
  • Reply