fsharp / fslang-suggestions

The place to make suggestions, discuss and vote on F# language and core library features
345 stars 21 forks source link

suggest: `valueof` operator to eval expression at compile time #804

Closed xp44mm closed 1 year ago

xp44mm commented 4 years ago

fsharp have added nameof operator in recent version. it is just a syntax sugar. but people like it. I propose we should be to do further more.

for example:

let name = "abc"
let length = 3 // value of name.length

maybe, we can do this:

let name = "abc"
let length = valueof name.Length

this code will be translated into previous code at compile time. it will reduce people a lot of typos. and more readable code.

maybe, we can get an inference:

purity function that take arguments that are all constant return constant result.

xp44mm commented 4 years ago

i think whilelist can very long. when you find some rule, then to shorten whilelist.

yatli commented 4 years ago

Another approach is that, transitivity is only a constraint within a function (to check one declared [<ConstExpr>] is really pure or not)

xp44mm commented 4 years ago

amost all of Excel function is pure function. can in whilelist.

Happypig375 commented 4 years ago

https://support.office.com/en-us/article/RAND-function-4CBFA695-8869-4788-8D90-021EA9F5BE73

xp44mm commented 4 years ago

@yatli manual mark pure function is good idea.

Happypig375 commented 4 years ago

Then the code in the first post will not work. Please update.

xp44mm commented 4 years ago

the excel function random, now row, column, address, caller is effect function. just several.

abelbraaksma commented 4 years ago

amost all of Excel function is pure function. can in whilelist

There's nothing in F# that should be compared to Excel. Excel is not even a programming language.

The problem in this discussion is one of definition. I think the OP wants auto-magic constants, where a let binding 'transpiles' to a constant.

But this can never, ever work, because a constant in the .NET world is replaced by its value when consumed. That means, if a.dll has a constant, and b.dll consumes this, and we now change the value in a.dll, it will not be reflected in b.dll until you recompile it.

Therefore, magically changing existing let bindings into constants is a no-go.

There is, however, something to say for constant evaluation of expressions, provided that the compiler can infer this. This isn't as hard as it looks and there's already a proposal somewhere for F#. We can just use Literal for it. Or a special operator or function that takes an expression and evaluates it at compile time (cannot be valueof, as that is not reserved).

array.map, array.reduce, is pure function.

They are not. They can change state by virtue of the mapper or folder function you use. Besides, their output is never constant (remember that compile time constants can only ever be the built in value types that are not structs, and strings; not even a decimal can be costant, for instance).

I agree that to pre-calculate one time evaluations can be valuable, but if they are expensive calculations, you should reconsider your design. If they aren't expensive, the benefit is minimal, as all given examples from the OP are already valid code. I like the idea of an expression that forces compile time evaluation, but by virtue of the limits of CLR, this will have a very limited set of allowed functions (and let's be fair, if you know beforehand that the length of a string in your code is constant for eternity, you can micro optimize yourself by replacing it with a constant, I think we need better use cases. Here's one: current time, which could be stored as ticks as int64).

abelbraaksma commented 4 years ago

It took me a moment to find it, but the proposal for allowing expressions in literals already exists and is approved. It just needs someone to work on it: https://github.com/fsharp/fslang-suggestions/issues/539

It's not exactly the same as the OP asks, but it goes a long way and could in time be expanded.

xp44mm commented 4 years ago

@abelbraaksma regex is another use case: it can be precompiled to DFA, and never changed if regex no changed.

i think literal more than constant, literal include constant you metioned and also include tuple literal list literal, optional literal, any immutable data struct's literal. even any can be serialized data. for example array literal json literal.

xp44mm commented 4 years ago

But this can never, ever work, because a constant in the .NET world is replaced by its value when consumed. That means, if a.dll has a constant, and b.dll consumes this, and we now change the value in a.dll, it will not be reflected in b.dll until you recompile it.

this do not matter. because the length value be infered or evaluated form "abc" literal, if length be updated in a.dll. then "abc" must be updated also in a.dll, so anyway, the b.dll must be recompiled.

abelbraaksma commented 4 years ago

i think literal more than constant, literal include constant you metioned and also include tuple literal list ....

Actually, no. In F#, a literal, declared with the [<Literal>] attribute, or inlined as a literal expression, is a constant. I mean, what other languages like C#, VB, C++ call constants, is called a Literal in F#.

And constants, or literals, by their very definition, cannot be reference types (the exception being strings and byte arrays). So your example of regex won't work, not even if you allow calculations of function calls in literals that are to be evaluated at compile time. The only operators presently allowed are ||| on numbers and + on strings. But have a look at the proposal on expanding the set of allowed operators, as I mentioned before. It would be a good starting point.

To get what you want with regexes, you can actually use the methods available from System.Text.RegularExpressions, like CompileToAssembly.

Wanting to allow a "constant" like that, i.e., a whole object, or in this case an actual assembly, to be pre-compiled and stored in your DLL or EXE would mean that somehow the compiler needs to serialize and deserialize the data that you you pre-computed. That's far from trivial, there are books written on that subject alone, and likely not going to happen (but I'm not the one who decides, if there's a reasonable proposal and it gets traction, and you're willing to implement it, they may accept it).

An alternative you might consider is using a RESX file. Create an auxiliary project that can do the pre-calculations that you want. Store them in a (binary) file. Read this in into the resx file as resources and access them directly from your code just like with C# projects. That is the proper way to go about resources (or "constants") that extend the realm of the expressive power or usefulness of Literal bindings in F#.

About regexes in particular, a good tool that I use is this Regular Expression Library Builder, which can do exactly what you suggest and create "constant", that is, DLL assemblies, of your compiled regular expressions.

xp44mm commented 4 years ago

@abelbraaksma thank you! actually i can work aroud it, the simplest is:

let len = 3 // abc.Length

or i can test it:

Assert.equal(len,abc.Length)

maybe i can just keep it no changed:

let len = abc.Length
abelbraaksma commented 4 years ago

this do not matter. because the length value be infered form "abc", if length be updated in a.dll. then "abc" must be updated also in a.dll, so anyway, the b.dll must be recompiled.

I'm not sure you understand how literals, or constants work. I don't understand what you mean with "infer" here. Let's say you have:

// a.dll
let [<Literal>] SomeString = "John"

// b.dll
let [<Literal>] LengthSomeString = SomeString.Length

// c.dll
let [<Literal>] DoubleLength = LengthSomeString * 2

If one company created a.dll another one b.dll and another one c.dll, if the first changes the name, and you have some code depending on this length, then everything falls apart (for instance, say you have code that iterates over all characters for i=0 to LengthSomeString do ... this will horribly fail).

This can also happen with actual constants and is a well documented fact and cause of confusion for C# developers. Allowing arbitrary expressions is potentially going to wreak havoc.

I still think there's value in an expression like valueof taking an argument that is evaluated at compile time, but it should be severely restricted. A current workaround, btw, is using generated files or something like Fody for weaving, or Cecil, which all allow much of what you are after and more. But each comes with their own caveats.

cartermp commented 4 years ago

I would think the function could only apply to things that you could also apply the [<Literal>] attribute to. Would that be unexpected, since it's so restrictive?

abelbraaksma commented 4 years ago

@cartermp, I think it should be allowed everywhere where we allow literals to appear (like "a string", 123y etc). Technically that's the same as bindings marked with Literal, as they simply get replaced inline, as opposed to referenced.

A large part of the discussion was about the confusion of what the OP wanted, which frankly, I'm still unsure about, but taken as a compile time evaluation of a valid F# expression that yields a .NET Framework allowed literal, I think it has merit.

xp44mm commented 4 years ago

@abelbraaksma yes! dependent on constant in another assembly, and fixed it to result value is dangous. maybe outdated. so i must sure another assembly is never absolutely changed for example constant PI. compiler don't recongnize this dangous situation. so, must manual mark need evaluation of expression at compile time.

xp44mm commented 4 years ago

@abelbraaksma regex is reference type in implement, but it is some immutable data in logic. so can resolve it in advance.

another use case: reflection is some immutable data, it represent source code, never changed except source code changed. it can be resolved in advance at compiler.

abelbraaksma commented 4 years ago

regex is reference type in implement, but it is some immutable data in logic. so can resolve it in advance.

Sorry, still not sure what you mean, as this is simply not possible given the current state of the art of compilers for the .NET platform. So either you mean something that I don't understand (examples, please), or I'm afraid you may not understand what the purpose, usages and limits of literals are, which is fine, but then I suggest you read the article I referenced earlier. The types there are the types any literal is limited to by the .NET Framework. So whatever your proposal, it has to be within those limits, or you would have to make your suggestion to Roslyn in stead.

reflection is some immutable data,

Nope, reflection is not immutable. That's why people use reflection: they only know the type signatures at runtime. For instance because the versions of the methods they inspect can be different, or because they use pluggable libraries.

You could do it once, serialize it (MethodInfo is serializable) and read this back in, but that only makes sense if you expect libraries to never be updated. But then again, serializing and deserializing is already possible, just like with regexes, you don't need extra language features for that.

xp44mm commented 4 years ago

hi! maybe this can spark your idea: https://babeljs.io/docs/en/babel-plugin-minify-constant-folding

abelbraaksma commented 4 years ago

Constant folding already happens (try 10 + 32), empty functions are optimized away, small functions are inlined, either by F# or by CLR. Is there a specific case where you don't see it happen and you would like it improved? I'm sure improvements are possible, but we'll need concrete use cases.

Swoorup commented 4 years ago

When using literals, I am rather looking for some sort of guarantees by the compiler to not process just once and cache it, but embed the value when compiling.

I think one approach to this would be marking functions as constfn as discussed here earlier and the compiler processes allows only a limited set of IL code, ie constfn function can't make call to non-const functions. Also when marking a variable as literal, it can only invoke constfn whose parameters are also literals.

abelbraaksma commented 4 years ago

but embed the value when compiling.

@Swoorup That currently happens if you mark something as Literal, though I assume you mean to extend the set of allowed operations inside a literal expression?

I believe IL limits what's allowed as constants: primitives, string, guid, type, byte array. Of these, F# doesn't support Type iirc. And I've read somewhere that decimals are soon to be allowed as constants too, at least in C#.

Any set of functions that is going to be allowed in constant folding operations must be limited to taking and returning said types, cannot have side effects and cannot use currying. Anything else should probably go through a user defined precompilation step.

This is just my take on this to keep it simple, though technically these limitations could be lifted to some extend. For one, it would be nice if struct DU and records were allowed here.

abelbraaksma commented 4 years ago

Considering what C# allows: I was wrong, only strings are reference types that are allowed, plus the native primitives. Arrays, guids and Types are not (though they are allowed by the IL, after all, you can use them in attributes).

Swoorup commented 4 years ago

@abelbraaksma That sounds similar to the approach rust takes with the exception of proc macros.

dsyme commented 1 year ago

I'm closing this, as arbitrary compile-time evaluation like in the OP isn't planned to be part of F#