{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Formatting Outputs \n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "When you return a value or a display a value in a .NET notebook, the default formatting uses HTML to render the object. For many values, a table structure is used by default. For example:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "display [\"hello\"; \"world\"]\n", "\n", "[ 1 .. 4 ]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Switching to plain text only\n", "\n", "If you prefer, you can switch to plain text by default. The simplest way to do this is to register `\"text/plain\"` as the default mime type for all objects and switch the `%A` printing. " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "Formatter.SetPreferredMimeTypeFor(typeof, \"text/plain\")\n", "Formatter.Register(fun (x:obj) (writer: TextWriter) -> fprintfn writer \"%120A\" x )\n", "\n", "[ 1 .. 4 ]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now reset back to defaults:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "Formatter.ResetToDefault()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## HTML formatting\n", "\n", "### Strings\n", "\n", "By default multi-line content does not have formatting preserved. For example:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "[ sprintf \"%120A\" [ for i in 1 .. 30 -> [ 1 .. i ]] ]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You can use styling to preserve the preformatting of such content." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "CSS \".dni-plaintext { text-align: left; white-space: pre; font-family: monospace; });\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note that `Formatter.ResetToDefault()` doesn't undo any CSS changes. You can undo this change with:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "CSS \".dni-plaintext { text-align: inherit; white-space: inherit; font-family: inherit; });\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Objects\n", "\n", "The default formatting behavior for many objects is to produce a table showing their properties and the values of those properties." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "type Person = { FirstName: string; LastName: string; Age: int }\n", "\n", "// Evaluate a new person\n", "{ FirstName = \"Mitch\"; LastName = \"Buchannon\"; Age = 42 }" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Sequences\n", "\n", "When you have a collection of objects, you can see the values listed for each item in the collection:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "[\n", " { FirstName = \"Mitch\"; LastName = \"Buchannon\"; Age = 42 }\n", " { FirstName = \"Hobie \"; LastName = \"Buchannon\"; Age = 23 }\n", " { FirstName = \"Summer\"; LastName = \"Quinn\"; Age = 25 }\n", " { FirstName = \"C.J.\"; LastName = \"Parker\"; Age = 23 }\n", "]\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Dictionaries\n", "\n", "Similarly to the behavior for `IEnumerable` objects, you'll also see table output for dictionaries, but for each value in the dictionary, the key is provided rather than the index within the collection." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "// Cannot simply use 'dict' here, see https://github.com/dotnet/interactive/issues/12\n", "\n", "dict [(\"zero\", 0); (\"one\", 1); (\"two\", 2)]\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Nested objects\n", "\n", "Now let's try something a bit more complex. Let's look at a graph of objects. \n", "\n", "We'll redefine the `Person` class to allow a reference to a collection of other `Person` instances." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "type Person =\n", " { FirstName: string\n", " LastName: string\n", " Age: int\n", " Friends: ResizeArray }\n", "\n", "let mitch = { FirstName = \"Mitch\"; LastName = \"Buchannon\"; Age = 42; Friends = ResizeArray() }\n", "let hobie = { FirstName = \"Hobie \"; LastName = \"Buchannon\"; Age = 23; Friends = ResizeArray() }\n", "let summer = { FirstName = \"Summer\"; LastName = \"Quinn\"; Age = 25; Friends = ResizeArray() }\n", "\n", "mitch.Friends.AddRange([ hobie; summer ])\n", "hobie.Friends.AddRange([ mitch; summer ])\n", "summer.Friends.AddRange([ mitch; hobie ])\n", "\n", "let people = [ mitch; hobie; summer ]\n", "display people" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "That's a bit hard to read, right? The defaut formatting behaviors are not always as useful as they might be. In order to give you more control in these kinds of cases, formatters can be customized from within the .NET notebook." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Customizing\n", "\n", "### Registering plain text formatters" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's clean up the output above by customizing the formatter for the `Person.Friends` property, which is creating a lot of noise. \n", "\n", "The way to do this is to use the `Formatter` API. This API lets you customize the formatting for a specific type. Since `Person.Friends` is a sequence of `Person` objects, i.e. type `seq`, we can register a custom formatter for that type to change the output. Let's just list their first names:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "Formatter.Register>(\n", " mimeType = \"text/plain\",\n", " formatter = fun context people (writer: TextWriter) ->\n", " for person in people do\n", " writer.Write(person.FirstName)\n", " writer.Write(\" \")\n", " true)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now display the `people` data again:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "people" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You might have noticed that `people` is of type `ResizeArray`, but the table output still includes columns for `LastName`, `Age`, and `Friends`. What's going on here? Notice that the custom formatter we just registered was registered for the mime type `\"text/plain\"`. The top-level formatter that's used when we call `display` requests output of mime type `\"text/html\"` and the nested objects are formatted using `\"text/plain\"`. It's the nested objects, not the top-level HTML table, that's using the custom formatter here. If you'd like to change the default mime type, use `SetPreferredMimeTypeFor`.\n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Registering HTML formatters\n", "\n", "To replace the default HTML view for a particular type, you can register a formatter for the `\"text/html\"` mime type. \n", "\n", "The HTML is specified using the HTML DSL similar to Giraffe's ViewEngine. Note: In this example we put the formatter registration in a module to prevent opening the `Html` DSL everywhere." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "module PersonHtmlFormatter = \n", " \n", " // Locally open the F# HTML DSL.\n", " open Html\n", "\n", " Formatter.Register>(\n", " mimeType = \"text/html\",\n", " formatter = fun (context: FormatContext) (people: seq) (writer: TextWriter) ->\n", " table [] [\n", " thead [ _style [\"color: blue\"]] [\n", " th [] [ str \"First Name\" ]\n", " th [] [ str \"Last Name\" ]\n", " th [] [ str \"Age\" ]\n", " ]\n", " tbody [_style [\"color: darkolivegreen\"]] [\n", " for p in people do\n", " tr [] [\n", " td [] [ str p.FirstName ]\n", " td [] [ str p.LastName ]\n", " td [] [ str (string p.Age) ; str \" years\" ] \n", " ]\n", " ]\n", " ]\n", " |> writer.Write\n", " true)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now display the `people` data again:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "people" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Registering formatters for generic types\n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "Formatters can be specified by using a generic type definition as a key, for example:\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "Formatter.Register(``type`` = typedefof>,\n", " formatter = Action(fun (value: obj) (writer: TextWriter) ->\n", " writer.Write(\"quack\")\n", " ), \n", " mimeType = \"text/html\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Then" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "new System.Collections.Generic.List()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Reflection can then be used to operate on the object at its more specific type." ] } ], "metadata": { "kernelspec": { "display_name": ".NET (F#)", "language": "F#", "name": ".net-fsharp" }, "language_info": { "file_extension": ".fs", "mimetype": "text/x-fsharp", "name": "C#", "pygments_lexer": "fsharp", "version": "4.5" } }, "nbformat": 4, "nbformat_minor": 4 }