SE-0246: Generic Math(s) Functions

I’m a little late to this, but I have a few opinions on this proposal.

  • I agree with a lot of the other people here that the .Math namespace is detrimental and unnecessary. This feels like namespacing for namespacing’s sake, and just adds meaningless noise to source code. It’s also inconsistent with the current homes of Float.pi and Double.e, and despite what some people are suggesting, we are not getting rid of those anytime soon, considering how widely used they are.

  • I’m gonna come out and say that I think the Math module providing the free functions just doesn’t make a lot of sense. It’s kind of pointless to import a whole module just to get access to abbreviated function names. I can see myself asking myself “will I be using sin enough in this project to justify importing Math, or should I just stick to the standard library Double.(Math.)sin(_:) function?” which doesn’t seem ideal
    The circumstances also seem pretty contrived,, the whole justification for free functions comes from the fact that Double.Math.cos(x) is so long to type, but that’s an argument for getting rid of the .Math namespace, not for introducing a whole new redundant module.

  • I see a lot of people bikeshedding the log vs ln name. I personally prefer ln but do y’all really use it often enough for it to make sense for it to dominate this much of the conversation?

6 Likes

This doesn't exist.

The expectation is that these names will not generally be used in source code. The free functions are the preferred spellings, these are implementation hooks to get customization, and an escape valve when you don't want to transitively export Math. I would underscore them, but the community has shown a pretty clear dislike of "magic" underscored trampolines in the past, so grouping them together under .Math seems like the best option to keep autocomplete lists clear.

2 Likes

I really like this improvement to Swift. I strongly approve of these elementary functions being available as free functions by importing Math. Compact notation is very important for ergonomics and readability in scientific/mathematical code. The video on the home page of julialang.org talks about exactly this and shows some great examples. Good ergonomics for scientific/mathematical code has traditionally been the domain of scripting languages but I see no reason why this has to be the case - Swift has the opportunity to unite these ergonomics with a full featured strongly typed application development language.

Honestly, I would prefer underscored trampolines over another namespace if it came down to the two. But there just aren’t that many static functions on these numeric types, so I really don’t see what we’d be clogging up.

7 Likes

I'd be concerned about us having too many different mechanisms for ad-hoc namespacing, because if we ever do get real namespacing, it'll become harder to migrate all the different ways we've found to express namespaces to the proper mechanism. Using an associated type as a namespace also feels particularly problematic to me because it has runtime ABI impact; the associated type needs an entry in the parent protocol's witness table, necessitates the existence of a second protocol and parallel set of conformances, and makes dispatching to functions defined on the associated type require an extra two steps to fetch the associated type then dispatch to the implementation on the associated type.

8 Likes

Isn't the alternative that all the implementation hooks need entries in the parent protocol's witness table, though? Is that better?

Can you clarify what you mean here? It requires a second protocol, but the conformances mostly only show up on that second protocol.

In practice, these get specialized and lowered to direct calls to the underlying libm functions. If you manage to construct a context where specialization doesn't occur, you're already dead because addition is a call; an extra layer or two of indirection for a call to exp( ) will be the least of you worries.

Performance isn’t really my primary concern. As you noted, the conformances are primarily intended to be used for specialization; nonetheless, the runtime data structures must exist. That’s my bigger concern—yes, the witness table entry must live somewhere, but if you’re breaking this up across two conformances, you’re paying twice as much fixed overhead for the protocol and conformance metadata. Furthermore, since this becomes part of the ABI, we can’t ever take it away completely if we have a better namespacing mechanism in the future.

1 Like

I don't see how you get to "twice as much" (feel free to explain offline if we're wandering into the weeds here).

Instead of one protocol and one conformance per type, you now have two protocols and two conformances per type, and therefore an extra protocol descriptor and a conformance descriptor plus witness table header of overhead per conforming type.

Isn't that O(1) cost dominated by the O(n) individual functions?

Yes, but all I’m saying is that there is an additional, permanent cost.

1 Like

oh rip. if you really think about it then though, it would make a lot of sense to add it, and phi to the standard library, and it would suck if pi lived in Double, but e and phi lived in Double.Math.

This proposal looks really good to me. I have a preference for ln over log (from my Physics background) but I would be more than happy to have both included, which I think gets the best of both worlds. Those of us who use ln would be able to do so, and vice versa.

I think this is far worse. I’d much rather have a single canonical name, than confusingly using two names for the same function. Even if that one name is log (shudders)

9 Likes

I don't really care where the functions live, but constants like this should imo be on the type itself, so we can use the leading-dot shorthand to refer to them.

7 Likes

Hi all --

I pushed some updates to the proposal and implementation based on feedback received so far. These changes are mainly around the division of API between ElementaryFunctions and Real:

  • atan2 , erf , erfc , gamma , and logGamma have been moved from ElementaryFunctions onto Real . The rationale for this is threefold: atan2 never made much sense for non-real arguments. Implementations of erf , erfc , gamma and logGamma are not available for complex types on all platforms. Finally, those four functions are not actually "elementary", so the naming is more accurate following this change.

  • hypot has been added to Real. I would still like to have a better pattern for this functionality in the future, but it's a sufficiently useful function that there's no reason not to provide it now.

Three requested changes that I haven't made, with rationale:

  • I have not made any change to the spelling of log. I was more or less neutral between the two names (ln is more precise, but loses the semantic grouping of the log prefix shared by all other logarithms), but @nevin's observation that ln is confusable with in in many fixed-width fonts tips the balance back towards log for me. In any event, this is a change that could be made post-review if the core team feels strongly in favor of ln.

  • I have also not added new constants. The only constant that I could make a totally clear-cut case for including is e, and spelling here becomes important. The leading-dot shorthand makes .e possibly too terse if defined on the type itself. .Math.e would be clearer, but doesn't actually compile presently (I think that's a bug, which I'll follow up on). I'm definitely not opposed to adding this once these issues are resolved.

  • I'm keeping the .Math prefix on the implementation hooks for now. The proposal can be made to work either way without any major technical issues, so this is mainly a question of style: "do we want to encourage static-member namespaces?"--and that's really a question for the core team.

As a reminder, review runs through next Wednesday. Please don't hesitate to ask if you have any questions about these changes.

9 Likes

I'm a belated but enthusiastic +1 for this proposal.

I'm a heavy numerical user of swift for stats / ml and have written a number of libraries I look forward to making generic over ElementaryFunctions. I do like that name, especially now that Error and Gamma have been moved out. Those two have often been put in what's been called "Special Functions" going back to the days of netlib and Numerical Recipes. I have a library (GitHub - axadam/Numerical: Common numerical functions and tools in Swift) which fills out more of the Special Functions and which I'm glad to see should sit very easily on top of these new types.

I also have gone back and forth over the namespace question with my own work. In practice I find myself always using the free function forms. Numerical libraries can quickly add a lot of functions to the top level namespace, but as long as they are a separate import that's probably what the user wants. I do appreciate the discoverability of the namespace approach.

Overall very pleased to see this being added!

1 Like

to be clear, are you taking about T.sin(x) vs sin(x), or T.Math.sin(x) vs sin(x)?

I suppose since I end up using sin(x) I shouldn't have a strong opinion on T.sin(x) vs T.Math.sin(x). I have a weak preference for T.sin(x) because it feels more discoverable. I also find "math" to be not a very distinguishing category of functionality on numeric types. But as I said, I don't really care as I probably won't access it this way.

I've thought about this quite a bit, and I'm going to give a very big -1 to this design. I want the functionality, but I don't think this design fits with Swift at all. It needs to be thoroughly revised.

I very much dislike the awkward .Math namespacing and duplicating everything as trivially-forwarding free functions. I can't think of any good reason for them to exist. The best answer I can find is that Steve said once that numerics people prefer the free functions. As much as I respect Steve's expertise in this area, I think that's just not good enough as a justification for doing things that way in Swift. Swift is a new language and we have our own ways of doing things. Types like Int and Double can have member functions, where they might not be able to in other languages. Numerics people using this language should ideally adapt their style to idiomatic Swift, not the other way around.

One of the patterns I've noticed in these discussions is everybody (even members of the core team) being cautious with their opinions and saying stuff like "I'm not a numerics expert, but...". Very few of us are numerics experts, and it's difficult to really judge what it means for Swift to be a "good numerics language". I think we are all qualified to judge what feels "Swifty", and for me this just isn't.

There are some serious problems with this design - I mean, take the protocol names ElementaryFunctions and Real - what are those names supposed to mean? It's absolutely not obvious what <T: Real> is. I feel it should at least be called RealNumber or something. The Math associated-type is totally bizarre.

Sorry, but as much as I want this functionality, there's no way I can support this proposal as-is.


P.S: Just to be clear: I'm mentioning Steve by name because he's our numerics expert and the author of the proposal, but of course this isn't anything personal. I'm sure nobody took it that way, but I just want to make sure there are no misunderstandings. I just don't like this design.

8 Likes