Skip to content

Add generic print and println functions to FSharp.Core#19265

Open
bbatsov wants to merge 3 commits intodotnet:mainfrom
bbatsov:feature/print-println
Open

Add generic print and println functions to FSharp.Core#19265
bbatsov wants to merge 3 commits intodotnet:mainfrom
bbatsov:feature/print-println

Conversation

@bbatsov
Copy link
Contributor

@bbatsov bbatsov commented Feb 9, 2026

Summary

This is another take on RFC FS-1125, following up on the closed #13597. I guess that's mostly an attempt to see if anyone's interested in driving the old proposal to the finish line. For me those would definitely be handy functions to have, but obviously they are not something very important.

The key difference from the original PR is that print and println are generic ('T -> unit) rather than string -> unit, addressing the feedback from @dsyme that a generic print function would be a better use of the "good name":

let print (value: 'T) = Console.Out.Write(string value)
let println (value: 'T) = Console.Out.WriteLine(string value)

Both functions use the existing string operator for conversion, which already:

  • Uses InvariantCulture for IFormattable types (consistent with printfn, sprintf, etc.)
  • Passes strings through as-is
  • Falls back to .ToString() for other types (F# types like Option, List, etc. have good .ToString() overrides)

Changes

  • Added print and println signatures with XML docs to fslib-extra-pervasives.fsi
  • Added implementations in fslib-extra-pervasives.fs
  • Added unit tests (PrintTests.fs) covering: string, int, float, bool, Option, None, list, newline behavior, and concatenation
  • Updated all 4 surface area baselines

Open questions

  • What about eprintf(n) and fprintf(n)? Do they generic counterparts as well?

@github-actions
Copy link
Contributor

github-actions bot commented Feb 9, 2026

❗ Release notes required


✅ Found changes and release notes in following paths:

Change path Release notes path Description
src/FSharp.Core docs/release-notes/.FSharp.Core/10.0.300.md

Comment on lines 280 to 287
[<CompiledName("PrintValue")>]
let print (value: 'T) =
Console.Out.Write(string value)

[<CompiledName("PrintValueLine")>]
let println (value: 'T) =
Console.Out.WriteLine(string value)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should functions be inline?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, @bbatsov pls mark at as inline so that it can specialize around the string call for certain types and avoid a .ToString() call.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do!

Add inline `print: 'T -> unit` and `println: 'T -> unit` to
ExtraTopLevelOperators. These use the existing `string` function for
conversion (InvariantCulture for IFormattable, .ToString() fallback)
and write to Console.Out.

RFC FS-1125
@bbatsov bbatsov force-pushed the feature/print-println branch from 53c25f7 to c00b32e Compare February 14, 2026 13:30
@vzarytovskii
Copy link
Member

Will also probably want IL baseline tests to make sure correct overloads are getting called.

Verify that inline specialization produces direct calls to
Int32.ToString, Double.ToString with InvariantCulture, and
Console.Out.Write/WriteLine for the respective types.
@bbatsov
Copy link
Contributor Author

bbatsov commented Feb 14, 2026

I've added some IL tests - hopefully I got those right. First contributions as always the hardest... 😅

@vzarytovskii
Copy link
Member

vzarytovskii commented Feb 14, 2026

I've added some IL tests - hopefully I got those right. First contributions as always the hardest... 😅

Yeah, those are the ones I meant. However, I do see that for some of them it is calling ToString for some value types (i32 for example), and calls string overload for the Write(Line)

@brianrourkeboll
Copy link
Contributor

However, I do see that for some of them it is calling ToString for some value types (i32 for example), and calls string overload for the Write(Line)

That's inherent to the approach taken in this PR (calling string on the input and passing it to Console.WriteLine), no?

We could delegate to the appropriate overload of Console.WriteLine at compile time by using static optimizations for common types (like the string function itself does).

@vzarytovskii
Copy link
Member

vzarytovskii commented Feb 14, 2026

However, I do see that for some of them it is calling ToString for some value types (i32 for example), and calls string overload for the Write(Line)

That's inherent to the approach taken in this PR (calling string on the input and passing it to Console.WriteLine), no?

Yes, I forgot to look at the implementation tbh.

We could delegate to the appropriate overload of Console.WriteLine at compile time by using static optimizations for common types (like the string function itself does).

We probably should, jit might lift some allocations to stack, but we shouldn't rely on it.

@bbatsov
Copy link
Contributor Author

bbatsov commented Feb 14, 2026

I think I'll need some more pointers about the next step, as I'm not quite sure how to proceed from here.

@Happypig375
Copy link
Member

@bbatsov consider looking at the implementation for the string function and use a similar syntax for Console.WriteLine overloads.

For culture-independent types (string, char, bool), bypass the
`string` operator and call the appropriate TextWriter.Write/WriteLine
overload directly.

Numeric types (int, float, etc.) must still go through `string` to
ensure InvariantCulture formatting, since TextWriter.Write(int/float)
uses the writer's FormatProvider which is CurrentCulture for Console.Out.

Add IL tests verifying char and bool use their direct overloads.
@bbatsov
Copy link
Contributor Author

bbatsov commented Feb 15, 2026

I've added static optimizations for string, char, and bool — these types are culture-independent, so we can call the corresponding TextWriter.Write/WriteLine overload directly, bypassing the string operator entirely.

For numeric types (int, float, decimal, etc.), we must continue going through the string operator to ensure InvariantCulture formatting. TextWriter.Write(int) internally calls value.ToString(FormatProvider), and Console.Out.FormatProvider is CultureInfo.CurrentCulture — so delegating directly would break InvariantCulture consistency for negative numbers and floating-point values in some locales.

The IL tests now verify:

  • int/floatToString(string, IFormatProvider) with InvariantCulture + Write(string)
  • string → direct Write(string) (no null check overhead)
  • char → direct Write(char)
  • bool → direct Write(bool)

I hope I got those right. I never thought a couple of print functions could be so involved. 😅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: New

Development

Successfully merging this pull request may close these issues.

5 participants