first commit cont.

This commit is contained in:
Meik Hellmund 2023-05-12 20:42:26 +02:00
parent 9f546da17c
commit cf80cf8a48
42 changed files with 7130 additions and 0 deletions

14
.gitignore vendored
View File

@ -1 +1,15 @@
/.quarto/
/_book/
/_freeze/
*.*~
.jupyter_cache
.ipynb_checkpoints
*_files
auto/
chapters/*.md
chapters/*.ipynb
*.bak
*.bak2
_extensions/julia/escfilter.sh
_extensions/julia/escfilter2.py
_extensions/julia/escfilter3.py

View File

@ -0,0 +1,6 @@
title: Code Visibility
author: fast.ai
version: 1.0.0
contributes:
filters:
- code-visibility.lua

View File

@ -0,0 +1,51 @@
-- remove any lines with the hide_line directive.
function CodeBlock(el)
if el.classes:includes('cell-code') then
el.text = filter_lines(el.text, function(line)
return not line:match("#| ?hide_line%s*$")
end)
return el
end
end
-- apply filter_stream directive to cells
function Div(el)
if el.classes:includes("cell") then
local filters = el.attributes["filter_stream"]
if filters then
-- process cell-code
return pandoc.walk_block(el, {
CodeBlock = function(el)
-- CodeBlock that isn't `cell-code` is output
if not el.classes:includes("cell-code") then
for filter in filters:gmatch("[^%s,]+") do
el.text = filter_lines(el.text, function(line)
return not line:find(filter, 1, true)
end)
end
return el
end
end
})
end
end
end
function filter_lines(text, filter)
local lines = pandoc.List()
local code = text .. "\n"
for line in code:gmatch("([^\r\n]*)[\r\n]") do
if filter(line) then
lines:insert(line)
end
end
return table.concat(lines, "\n")
end

View File

@ -0,0 +1,15 @@
title: Extensions for Julia
author: Meik Hellmund
version: 0.3.0
contributes:
format:
common:
filters:
- ansi2htmltex.lua
ipynb-filters:
- escfilter.py
pdf:
default
html:
monofont: "JuliaMono"

View File

@ -0,0 +1,390 @@
-- this is essentially
-- https://github.com/jupyter/nbconvert/blob/main/nbconvert/filters/ansi.py
-- converted to lua
-- good list of ANSI escape sequences:
-- https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797
local ANSI_COLORS = {
"ansi-black",
"ansi-red",
"ansi-green",
"ansi-yellow",
"ansi-blue",
"ansi-magenta",
"ansi-cyan",
"ansi-white",
"ansi-black-intense",
"ansi-red-intense",
"ansi-green-intense",
"ansi-yellow-intense",
"ansi-blue-intense",
"ansi-magenta-intense",
"ansi-cyan-intense",
"ansi-white-intense"
}
local function get_extended_color(numbers)
local n = table.remove(numbers, 1)
local r,g,b,idx
if n == 2 and #numbers >=3 then
-- 24bit RGB
r = table.remove(numbers, 1)
g = table.remove(numbers, 1)
b = table.remove(numbers, 1)
elseif n == 5 and #numbers >= 1 then
-- 256 colors
idx = table.remove(numbers, 1)
if idx < 16 then
-- 16 default terminal colors
return idx
elseif idx < 232 then
-- 6x6x6 color cube, see http://stackoverflow.com/a/27165165/500098
r = (idx - 16) // 36
r = 55 + r * 40
if r < 0 then r = 0 end
g = ((idx - 16) % 36) // 6
g = 55 + g * 40
if g < 0 then g = 0 end
b = (idx - 16) % 6
b = 55 + b * 40
if b < 0 then b = 0 end
elseif idx < 256 then
-- grayscale, see http://stackoverflow.com/a/27165165/500098
r = (idx - 232) * 10 + 8
g = r
b = r
end
end
return {r, g, b}
end
--[=[
local re = require "re"
local ANSI = re.compile [[
'\x1b%[' {.*?} {[@-~]}
]]
--]=]
local function LaTeXconverter(fg, bg, bold, underline, inverse)
if not (fg or bg or bold or underline or inverse) then
return "",""
end
local starttag = ""
local endtag = ""
if inverse then
fg, bg = bg, fg
end
if type(fg) == "number" then
starttag = starttag .. [[\textcolor{]] .. ANSI_COLORS[fg+1] .. "}{"
endtag = "}" .. endtag
elseif type(fg) == "table" then
-- See http://tex.stackexchange.com/a/291102/13684
starttag = starttag .. [[\def\tcRGB{\textcolor[RGB]}\expandafter]]
starttag = starttag .. string.format([[\tcRGB\expandafter{\detokenize{%d,%d,%d}}{]], fg[1], fg[2], fg[3])
endtag = "}" .. endtag
elseif inverse then
starttag = starttag .. [[\textcolor{ansi-default-inverse-fg}{]]
endtag = "}" .. endtag
end
if type(bg) == "number" then
starttag = starttag .. [[\setlength{\fboxsep}{0pt}]]
starttag = starttag .. [[\colorbox{]] .. ANSI_COLORS[bg+1] .. "}{"
endtag = [[\strut}]] .. endtag
elseif type(bg) == "table" then
-- See http://tex.stackexchange.com/a/291102/13684
starttag = starttag .. [[\setlength{\fboxsep}{0pt}]]
starttag = starttag .. [[\def\cbRGB{\colorbox[RGB]}\expandafter]]
starttag = starttag .. string.format([[\cbRGB\expandafter{\detokenize{%d,%d,%d}}{]], bg[1], bg[2], bg[3])
endtag = [[\strut}]] .. endtag
elseif inverse then
starttag = starttag .. [[\setlength{\fboxsep}{0pt}]]
starttag = starttag .. [[\colorbox{ansi-default-inverse-bg}{]]
endtag = [[\strut}]] .. endtag
end
if bold then
starttag = starttag .. [[\textbf{]]
endtag = "}" .. endtag
end
if underline then
starttag = starttag .. [[\underline{]]
endtag = "}" .. endtag
end
return starttag, endtag
end
local function HTMLconverter(fg, bg, bold, underline, inverse)
if not (fg or bg or bold or underline or inverse) then
return "",""
end
local classes = {}
local styles = {}
local type = type -- more efficient?
local next = next
if inverse then
fg, bg = bg, fg
end
if type(fg) == "number" then
table.insert(classes, ANSI_COLORS[fg+1] .. "-fg")
elseif type(fg) == "table" then
table.insert(styles, string.format("color: rgb(%d,%d,%d)", fg[1], fg[2], fg[3]))
elseif inverse then
table.insert(classes, "ansi-default-inverse-fg")
end
if type(bg) == "number" then
table.insert(classes, ANSI_COLORS[bg+1] .. "-bg")
elseif type(bg) == "table" then
table.insert(styles, string.format("background-color: rgb(%d,%d,%d)",
bg[1], bg[2], bg[3]))
elseif inverse then
table.insert(classes, "ansi-default-inverse-bg")
end
if bold then
table.insert(classes, "ansi-bold")
end
if underline then
table.insert(classes, "ansi-underline")
end
local starttag = "<span"
if next(classes) ~= nil then
starttag = starttag .. ' class="' .. table.concat(classes, " ") .. '"'
end
if next(styles) ~= nil then
starttag = starttag .. ' style="' .. table.concat(styles, " ") .. '"'
end
return starttag..">","</span>"
end
local function codeBlockTrans(e)
local converter, fmt
if quarto.doc.isFormat('latex') then
converter = LaTeXconverter
fmt = 'latex'
elseif quarto.doc.isFormat('html') then
converter = HTMLconverter
fmt = 'html'
else
return
end
-- not for input cells
if e.classes:includes("julia") or e.classes:includes("cell-code") then
return
end
if #e.classes > 0 and not e.classes:includes("julia-stderr") then
return
end
local texenv="OutputCell"
local codeclass=""
if string.find(e.text, "\u{a35f}\u{2983}") then
texenv = "AnsiOutputCell"
codeclass = "ansi"
end
if e.classes:includes("julia-stderr") then
texenv = "StderrOutputCell"
codeclass = codeclass .. " julia-stderr" -- empty leading space doesn't matter
end
local out=""
-- if string.find(e.text, "\x1b%[") then
if string.find(e.text, "\u{a35f}\u{2983}") then
local bold = false
local underline = false
local inverse = false
local text = e.text
local chunk = ""
local fg = nil
local bg = nil
local starttag = ""
local endtag = ""
local numbers={}
while text ~= "" do
numbers = {}
-- local s1, e1, c1, d1 = string.find(text, "\x1b%[(.-)([@-~])")
local s1, e1, c1, d1 = string.find(text, "\u{a35f}\u{2983}(.-)([@-~])")
if s1 then
if d1 == "m" then
for i in string.gmatch(c1, "([^;]*)") do
table.insert(numbers, tonumber(i))
end
else
quarto.log.warning("Unsupported ANSI sequence ESC["..c1..d1.." ignored\n" )
end
chunk, text = text:sub(1, s1-1), text:sub(e1+1)
else
chunk, text = text, ""
end
if chunk ~= "" then
if bold and type(fg)=="number" and fg<8 then
starttag, endtag = converter(fg+8, bg, bold, underline, inverse)
else
starttag, endtag = converter(fg, bg, bold, underline, inverse)
end
out = out .. starttag .. chunk .. endtag
end
while next(numbers) ~= nil do
local n = table.remove(numbers, 1)
if n == 0 then
fg = nil
bg = nil
bold = false
inverse = false
underline = false
elseif n == 1 then
bold = true
elseif n == 4 then
underline = true
elseif n == 5 then
bold = true -- 'blinking'
elseif n == 7 then
inverse = true
elseif n == 21 or n == 22 then
bold = false
elseif n == 24 then
underline = false
elseif n == 27 then
inverse = false
elseif n >= 30 and n <= 37 then
fg = n - 30
elseif n == 38 then
fg = get_extended_color(numbers)
elseif n == 39 then
fg = nil
elseif n >= 40 and n <= 47 then
bg = n - 40
elseif n == 48 then
bg = get_extended_color(numbers)
elseif n == 49 then
bg = nil
elseif n >= 90 and n <= 97 then
fg = n + 8 - 90
elseif n >= 100 and n <= 107 then
bg = n + 8 - 100
else
quarto.log.warning(string.format("ESC sequence with unknown code %d before:\n",n))
quarto.log.warning(chunk.."\n")
end
end
end
else
out = e.text
end
if fmt == 'html' then
return pandoc.RawBlock(fmt,
'<pre class="' .. codeclass ..'"><code class="' .. codeclass .. ' ansi">'..out..'</code></pre>')
end
if fmt == 'latex' then
return pandoc.RawBlock(fmt, [[\begin{]]..texenv.."}\n"..out.."\n"..[[\end{]].. texenv .. "}")
end
end
-- if div has class 'cell-output-stderr', give CodeBlocks in this div the class 'julia-stderr'
local function divStderr(e)
if e.classes:includes("cell-output-stderr") then
local c = e.content
for i,el in pairs(c) do
if el.t == 'CodeBlock' then
el.classes:insert("julia-stderr")
end
end
return e
end
end
-- repair julia ? output
local function divCodeBlockNoHeader1(e)
if not e.classes:includes("cell-output") then
return
end
local c = e.content
for i, el in pairs(c) do
if el.t == 'Header' then
el.level = 6
-- elneu = pandoc.Para(el.content)
-- c[i] = elneu
end
if el.t == 'CodeBlock' then
if el.classes:includes("jldoctest") then
x,i = el.classes:find("jldoctest")
el.classes:remove(i)
end
end
end
return e
end
-- test if two divs should be merged
local function testmerge(d1, d2)
return d1 and d1.t == "Div" and d1.classes:includes("cell-output") and #d1.content == 1
and d2 and d2.t == "Div" and d2.classes:includes("cell-output") and #d2.content == 1
and d1.content[1].t == "CodeBlock" and not d1.classes:includes("cell-output-stderr")
and d2.content[1].t == "CodeBlock" and not d2.classes:includes("cell-output-stderr")
end
-- merge (div (codecell (text1)), div (codecell(text2))) to div(codecell(text1+text2))
local function blockMerge(es)
local nl = ""
for i = #es-1, 1, -1 do
if testmerge(es[i], es[i+1]) then
str1 = es[i].content[1].text
str2 = es[i+1].content[1].text
nl = "\n"
if es[i].classes:includes("cell-output-stdout") and es[i+1].classes:includes("cell-output-stdout") then
if str1:sub(-1) == "\n" then
nl = ""
end
if str2:sub(1, 1) == "\n" then
nl = ""
end
end
es[i].content[1].text = str1 .. nl .. str2
es:remove(i+1)
end
end
return es
end
local function metaAdd(meta)
--for key, val in pairs(PANDOC_READER_OPTIONS) do
-- quarto.log.warning(key, val)
--end
quarto.doc.addHtmlDependency({name='ansicolors',
stylesheets = {'resources/css/ansicolor.css'}})
quarto.doc.addHtmlDependency({name='juliamonofont',
stylesheets = {'resources/css/juliamono.css'}})
if quarto.doc.isFormat('latex') then
quarto.doc.include_file("in-header", "resources/juliainc.tex")
end
end
return {
{Div = divStderr},
{Div = divCodeBlockNoHeader1},
{Blocks = blockMerge},
{CodeBlock = codeBlockTrans},
{Meta = metaAdd}
}

5
_extensions/julia/escfilter.py Executable file
View File

@ -0,0 +1,5 @@
import sys
f = sys.stdin.read()
g = f.replace('\\u001b[','ꍟ⦃')
sys.stdout.write(g)

View File

@ -0,0 +1,50 @@
/* CSS added by ANSI escape sequences filter */
.ansi {
/* line-height:1.15; */
overflow:auto;
}
.ansitight *.ansi {
line-height: 1.05;
}
.julia-stderr { background-color: #f8d6da; }
/* console foregrounds and backgrounds */
pre .ansi-black-fg { color: #3e424d; }
pre .ansi-red-fg { color: #e75c58; }
pre .ansi-green-fg { color: #00a250; }
pre .ansi-yellow-fg { color: #ddb62b; }
pre .ansi-blue-fg { color: #208ffb; }
pre .ansi-magenta-fg { color: #d160c4; }
pre .ansi-cyan-fg { color: #60c6c8; }
pre .ansi-white-fg { color: #c5c1b4; }
pre .ansi-black-bg { background-color: #3e424d; }
pre .ansi-red-bg { background-color: #e75c58; }
pre .ansi-green-bg { background-color: #00a250; }
pre .ansi-yellow-bg { background-color: #ddb62b; }
pre .ansi-blue-bg { background-color: #208ffb; }
pre .ansi-magenta-bg { background-color: #d160c4; }
pre .ansi-cyan-bg { background-color: #60c6c8; }
pre .ansi-white-bg { background-color: #c5c1b4; }
pre .ansi-black-intense-fg { color: #282c36; }
pre .ansi-red-intense-fg { color: #b22b31; }
pre .ansi-green-intense-fg { color: #007427; }
pre .ansi-yellow-intense-fg { color: #b27d12; }
pre .ansi-blue-intense-fg { color: #0065ca; }
pre .ansi-magenta-intense-fg { color: #a03196; }
pre .ansi-cyan-intense-fg { color: #258f8f; }
pre .ansi-white-intense-fg { color: #a1a6b2; }
pre .ansi-black-intense-bg { background-color: #282c36; }
pre .ansi-red-intense-bg { background-color: #b22b31; }
pre .ansi-green-intense-bg { background-color: #007427; }
pre .ansi-yellow-intense-bg { background-color: #b27d12; }
pre .ansi-blue-intense-bg { background-color: #0065ca; }
pre .ansi-magenta-intense-bg { background-color: #a03196; }
pre .ansi-cyan-intense-bg { background-color: #258f8f; }
pre .ansi-white-intense-bg { background-color: #a1a6b2; }
pre .ansi-default-inverse-fg { color: rgba(255, 255, 255, 1); }
pre .ansi-default-inverse-bg { background-color: #111111; }
pre .ansi-bold { font-weight: bold; }
pre .ansi-underline { text-decoration: underline; }

View File

@ -0,0 +1,55 @@
@font-face {
font-family: JuliaMono;
font-weight: 300;
font-style: normal;
src: url("../fonts/JuliaMono-Light.woff2") format("woff2");
text-rendering: optimizeLegibility;
}
@font-face {
font-family: JuliaMono;
font-weight: 400;
font-style: normal;
src: url("../fonts/JuliaMono-Regular.woff2") format("woff2");
text-rendering: optimizeLegibility;
}
@font-face {
font-family: JuliaMono;
font-weight: 500;
font-style: normal;
src: url("../fonts/JuliaMono-Medium.woff2") format("woff2");
text-rendering: optimizeLegibility;
}
@font-face {
font-family: JuliaMono;
font-weight: 600;
font-style: normal;
src: url("../fonts/JuliaMono-SemiBold.woff2") format("woff2");
text-rendering: optimizeLegibility;
}
@font-face {
font-family: JuliaMono;
font-weight: 700;
font-style: normal;
src: url("../fonts/JuliaMono-Bold.woff2") format("woff2");
text-rendering: optimizeLegibility;
}
@font-face {
font-family: JuliaMono;
font-weight: 800;
font-style: normal;
src: url("../fonts/JuliaMono-ExtraBold.woff2") format("woff2");
text-rendering: optimizeLegibility;
}
@font-face {
font-family: JuliaMono;
font-weight: 900;
font-style: normal;
src: url("../fonts/JuliaMono-Black.woff2") format("woff2");
text-rendering: optimizeLegibility;
}

Binary file not shown.

View File

@ -0,0 +1,93 @@
Copyright (c) 2020 - 2023, cormullion
with Reserved Font Name JuliaMono.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@ -0,0 +1,38 @@
\usepackage{fancyvrb}
\usepackage{xcolor}
\usepackage{fontspec}
\setmonofont{JuliaMono}[
Scale = MatchLowercase,
UprightFont = *-Regular,
BoldFont = *-Bold,
ItalicFont = *-RegularItalic,
BoldItalicFont = *-BoldItalic,
Contextuals = AlternateOff,
]
\DefineVerbatimEnvironment{OutputCell}{Verbatim}%
{xleftmargin=1.5em}
\DefineVerbatimEnvironment{AnsiOutputCell}{Verbatim}%
{commandchars=\\\{\}, xleftmargin=1.5em}
\DefineVerbatimEnvironment{StderrOutputCell}{Verbatim}%
{commandchars=\\\{\}, xleftmargin=1.5em,frame=single,rulecolor=\color{red}}
\definecolor{ansi-black}{HTML}{3E424D}
\definecolor{ansi-black-intense}{HTML}{282C36}
\definecolor{ansi-red}{HTML}{E75C58}
\definecolor{ansi-red-intense}{HTML}{B22B31}
\definecolor{ansi-green}{HTML}{00A250}
\definecolor{ansi-green-intense}{HTML}{007427}
\definecolor{ansi-yellow}{HTML}{DDB62B}
\definecolor{ansi-yellow-intense}{HTML}{B27D12}
\definecolor{ansi-blue}{HTML}{208FFB}
\definecolor{ansi-blue-intense}{HTML}{0065CA}
\definecolor{ansi-magenta}{HTML}{D160C4}
\definecolor{ansi-magenta-intense}{HTML}{A03196}
\definecolor{ansi-cyan}{HTML}{60C6C8}
\definecolor{ansi-cyan-intense}{HTML}{258F8F}
\definecolor{ansi-white}{HTML}{C5C1B4}
\definecolor{ansi-white-intense}{HTML}{A1A6B2}
\definecolor{ansi-default-inverse-fg}{HTML}{FFFFFF}
\definecolor{ansi-default-inverse-bg}{HTML}{000000}

View File

@ -0,0 +1,9 @@
title: LaTeX Environment
author: RStudio, PBC
version: 1.1.1
quarto-required: ">=1.2.198"
contributes:
filters:
- latex-environment.lua
format:
pdf: default

View File

@ -0,0 +1,133 @@
-- environment.lua
-- Copyright (C) 2020 by RStudio, PBC
local classEnvironments = pandoc.MetaMap({})
local classCommands = pandoc.MetaMap({})
-- helper that identifies arrays
local function tisarray(t)
local i = 0
for _ in pairs(t) do
i = i + 1
if t[i] == nil then return false end
end
return true
end
-- reads the environments
local function readEnvironments(meta)
local env = meta['environments']
if env ~= nil then
if tisarray(env) then
-- read an array of strings
for i, v in ipairs(env) do
local value = pandoc.utils.stringify(v)
classEnvironments[value] = value
end
else
-- read key value pairs
for k, v in pairs(env) do
local key = pandoc.utils.stringify(k)
local value = pandoc.utils.stringify(v)
classEnvironments[key] = value
end
end
end
end
local function readCommands(meta)
local env = meta['commands']
if env ~= nil then
if tisarray(env) then
-- read an array of strings
for i, v in ipairs(env) do
local value = pandoc.utils.stringify(v)
classCommands[value] = value
end
else
-- read key value pairs
for k, v in pairs(env) do
local key = pandoc.utils.stringify(k)
local value = pandoc.utils.stringify(v)
classCommands[key] = value
end
end
end
end
local function readEnvsAndCommands(meta)
readEnvironments(meta)
readCommands(meta)
end
-- use the environments from metadata to
-- emit a custom environment for latex
local function writeEnvironments(divEl)
if quarto.doc.is_format("latex") then
for k, v in pairs(classEnvironments) do
if divEl.attr.classes:includes(k) then
-- process this into a latex environment
local beginEnv = '\\begin' .. '{' .. v .. '}'
local endEnv = '\n\\end{' .. v .. '}'
-- check if custom options or arguments are present
-- and add them to the environment accordingly
local opts = divEl.attr.attributes['options']
if opts then
beginEnv = beginEnv .. '[' .. opts .. ']'
end
local args = divEl.attr.attributes['arguments']
if args then
beginEnv = beginEnv .. '{' .. args .. '}'
end
-- if the first and last div blocks are paragraphs then we can
-- bring the environment begin/end closer to the content
if #divEl.content > 0 and divEl.content[1].t == "Para" and divEl.content[#divEl.content].t == "Para" then
table.insert(divEl.content[1].content, 1, pandoc.RawInline('tex', beginEnv .. "\n"))
table.insert(divEl.content[#divEl.content].content, pandoc.RawInline('tex', "\n" .. endEnv))
else
table.insert(divEl.content, 1, pandoc.RawBlock('tex', beginEnv))
table.insert(divEl.content, pandoc.RawBlock('tex', endEnv))
end
return divEl
end
end
end
end
-- use the environments from metadata to
-- emit a custom environment for latex
local function writeCommands(spanEl)
if quarto.doc.is_format("latex") then
for k, v in pairs(classCommands) do
if spanEl.attr.classes:includes(k) then
-- resolve the begin command
local beginCommand = pandoc.RawInline('latex', '\\' .. pandoc.utils.stringify(v) .. '{')
local opts = spanEl.attr.attributes['options']
if opts then
beginCommand = pandoc.RawInline('latex', '\\' .. pandoc.utils.stringify(v) .. '[' .. opts .. ']{')
end
-- the end command
local endCommand = pandoc.RawInline('latex', '}')
-- attach the raw inlines to the span contents
local result = spanEl.content
table.insert(result, 1, beginCommand)
table.insert(result, endCommand)
return result
end
end
end
end
-- Run in two passes so we process metadata
-- and then process the divs
return {
{ Meta = readEnvsAndCommands },
{ Div = writeEnvironments, Span = writeCommands }
}

523
chapters/10_Strings.qmd Normal file
View File

@ -0,0 +1,523 @@
# Zeichen, Strings und Unicode
## Zeichencodierungen (Frühgeschichte)
Es gab - abhängig von Hersteller, Land, Programmiersprache, Betriebsssystem,... - eine große Vielzahl von Codierungen.
Bis heute relevant sind:
### ASCII
Der _American Standard Code for Information Interchange_ wurde 1963 in den USA als Standard veröffentlicht.
- Er definiert $2^7=128$ Zeichen, und zwar:
- 33 Steuerzeichen, wie `newline`, `escape`, `end of transmission/file`, `delete`
- 95 graphisch darstellbare Zeichen:
- 52 lateinische Buchstaben `a-z, A-Z`
- 10 Ziffern `0-9`
- 7 Satzzeichen `.,:;?!"`
- 1 Leerzeichen ` `
- 6 Klammern `[{()}]`
- 7 mathematische Operationen `+-*/<>=`
- 12 Sonderzeichen ``` #$%&'\^_|~`@ ```
- ASCII ist heute noch der "kleinste gemeinsame Nenner" im Codierungs-Chaos.
- Die ersten 128 Unicode-Zeichen sind identisch mit ASCII.
### ISO 8859-Zeichensätze
- ASCII nutzt nur 7 Bits.
- In einem Byte kann man durch Setzen des 8. Bits weitere 128 Zeichen unterbringen.
- 1987/88 wurden im ISO 8859-Standard verschiedene 1-Byte-Codierungen festgelegt, die alle ASCII-kompatibel sind, darunter:
:::{.narrow}
|Codierung | Region | Sprachen|
|:-----------|:----------|:-------|
|ISO 8859-1 (Latin-1) | Westeuropa | Deutsch, Französisch,...,Isländisch
|ISO 8859-2 (Latin-2) | Osteuropa | slawische Sprachen mit lateinischer Schrift
|ISO 8859-3 (Latin-3) | Südeuropa | Türkisch, Maltesisch,...
|ISO 8859-4 (Latin-4) | Nordeuropa | Estnisch, Lettisch, Litauisch, Grönländisch, Sami
|ISO 8859-5 (Latin/Cyrillic) | Osteuropa | slawische Sprachen mit kyrillischer Schrift
|ISO 8859-6 (Latin/Arabic) | |
|ISO 8859-7 (Latin/Greek) | |
|...| |
|ISO 8859-15 (Latin-9)| | 1999: Revision von Latin-1: jetzt mit Euro-Zeichen!
:::
## Unicode
Das Ziel des Unicode-Consortiums ist eine einheitliche Codierung für alle Schriften der Welt.
- Unicode Version 1 erschien 1991
- Unicode Version 15 erschien 2021 mit 149 186 Zeichen (das sind 4489 mehr als Unicode 14), darunter:
- 161 Schriften
- mathematische und technische Symbole
- Emojis und andere Symbole, Steuer- und Formatierungszeichen
- davon entfallen über 90 000 Zeichen auf die CJK-Schriften (Chinesisch/Japanisch/Koreanisch)
### Technische Details
- Jedem Zeichen wird ein `codepoint` zugeordnet. Das ist einfach eine fortlaufende Nummer.
- Diese Nummer wird hexadezimal notiert
- entweder 4-stellig als `U+XXXX` (0-te Ebene)
- oder 5...6-stellig als `U+XXXXXX` (weitere Ebenen)
- Jede Ebene geht von `U+XY0000` bis `U+XYFFFF`, kann also $2^{16}=65\;534$ Zeichen enthalten.
- Vorgesehen sind bisher 17 Ebenen `XY=00` bis `XY=10`, also der Wertebereich von `U+0000` bis `U+10FFFF`.
- Damit sind maximal 21 Bits pro Zeichen nötig.
- Die Gesamtzahl der damit möglichen Codepoints ist etwas kleiner als 0x10FFFF, da aus technischen Gründen gewisse Bereiche nicht verwendet werden. Sie beträgt etwa 1.1 Millionen, es ist also noch viel Platz.
- Bisher wurden nur Codepoints aus den Ebenen
- Ebene 0 = BMP _Basic Multilingual Plane_ `U+0000 - U+FFFF`,
- Ebene 1 = SMP _Supplementary Multilingual Plane_ `U+010000 - U+01FFFF`,
- Ebene 2 = SIP _Supplementary Ideographic Plane_ `U+020000 - U+02FFFF`,
- Ebene 3 = TIP _Tertiary Ideographic Plane_ `U+030000 - U+03FFFF` und
- Ebene 14 = SSP _Supplementary Special-purpose Plane_ `U+0E0000 - U+0EFFFF` vergeben.
- `U+0000` bis `U+007F` ist identisch mit ASCII
- `U+0000` bis `U+00FF` ist identisch mit ISO 8859-1 (Latin-1)
### Eigenschaften von Unicode-Zeichen
Im Standard wird jedes Zeichen beschrieben duch
- seinen Codepoint (Nummer)
- einen Namen (welcher nur aus ASCII-Großbuchstaben, Ziffern und Minuszeichen besteht) und
- verschiedene Attributen wie
- Laufrichtung der Schrift
- Kategorie: Großbuchstabe, Kleinbuchstabe, modifizierender Buchstabe, Ziffer, Satzzeichen, Symbol, Seperator,....
Im Unicode-Standard sieht das dann so aus (zur Vereinfachung nur Codepoint und Name):
```
...
U+0041 LATIN CAPITAL LETTER A
U+0042 LATIN CAPITAL LETTER B
U+0043 LATIN CAPITAL LETTER C
U+0044 LATIN CAPITAL LETTER D
...
U+00E9 LATIN SMALL LETTER E WITH ACUTE
U+00EA LATIN SMALL LETTER E WITH CIRCUMFLEX
...
U+0641 ARABIC LETTER FEH
U+0642 ARABIC LETTER QAF
...
U+21B4 RIGHTWARDS ARROW WITH CORNER DOWNWARDS
...
```
Wie sieht 'RIGHTWARDS ARROW WITH CORNER DOWNWARDS' aus?
```{julia}
'\U21b4'
```
### Eine Auswahl an Schriften
::: {.content-visible when-format="html"}
:::{.callout-note}
Falls im Folgenden einzelne Zeichen oder Schriften in Ihrem Browser nicht darstellbar sind, müssen Sie geeignete
Fonts auf Ihrem Rechner installieren.
Alternativ können Sie die PDF-Version dieser Seite verwenden. Dort sind alle Fonts eingebunden.
:::
:::
Eine kleine Hilfsfunktion:
```{julia}
"""
printuc(c, n):
print n characters from unicode table, starting with character c
"""
function printuc(c, n)
for i in 0:n-1
print(c + i)
end
end
```
__Kyrillisch__
```{julia}
printuc('\U0400', 100)
```
__Tamilisch__
:::{.cellmerge}
```{julia}
#| echo: true
#| output: false
printuc('\U0be7',20)
```
\begingroup\setmonofont{Noto Sans Tamil}
```{julia}
#| echo: false
#| output: true
printuc('\U0be7',20)
```
\endgroup
:::
__Schach__
```{julia}
printuc('\U2654', 12)
```
__Mathematische Operatoren__
```{julia}
printuc('\U2200', 255)
```
__Runen__
```{julia}
printuc('\U16a0', 40)
```
:::{.cellmerge}
__Scheibe (Diskus) von Phaistos__
- Diese Schrift ist nicht entziffert.
- Es ist unklar, welche Sprache dargestellt wird.
- Es gibt nur ein einziges Dokument in dieser Schrift: die Tonscheibe von Phaistos aus der Bronzezeit
```{julia}
#| echo: true
#| output: false
printuc('\U101D0', 46 )
```
\begingroup\setmonofont{Phaistos.otf}
```{julia}
#| echo: false
#| output: true
printuc('\U101D0', 46 )
```
\endgroup
:::
### Unicode transformation formats: UTF-8, UTF-16, UTF-32
_Unicode transformation formats_ legen fest, wie eine Folge von Codepoints als eine Folge von Bytes dargestellt wird.
Da die Codepoints unterschiedlich lang sind, kann man sie nicht einfach hintereinander schreiben. Wo hört einer auf und fängt der nächste an?
- __UTF-32__: Das einfachste, aber auch speicheraufwändigste, ist, sie alle auf gleiche Länge zu bringen. Jeder Codepoint wird in 4 Bytes = 32 Bit kodiert.
- Bei __UTF-16__ wird ein Codepoint entweder mit 2 Bytes oder mit 4 Bytes dargestellt.
- Bei __UTF-8__ wird ein Codepoint mit 1,2,3 oder 4 Bytes dargestellt.
- __UTF-8__ ist das Format mit der höchsten Verbreitung. Es wird auch von Julia verwendet.
### UTF-8
- Für jeden Codepoint werden 1, 2, 3 oder 4 volle Bytes verwendet.
- Bei einer Codierung mit variabler Länge muss man erkennen können, welche Bytefolgen zusammengehören:
- Ein Byte der Form 0xxxxxxx steht für einen ASCII-Codepoint der Länge 1.
- Ein Byte der Form 110xxxxx startet einen 2-Byte-Code.
- Ein Byte der Form 1110xxxx startet einen 3-Byte-Code.
- Ein Byte der Form 11110xxx startet einen 4-Byte-Code.
- Alle weiteren Bytes eines 2-,3- oder 4-Byte-Codes haben die Form 10xxxxxx.
- Damit ist der Platz, der für den Codepoint zur Verfügung steht (Anzahl der x):
- Ein-Byte-Code: 7 Bits
- Zwei-Byte-Code: 5 + 6 = 11 Bits
- Drei-Byte-Code: 4 + 6 + 6 = 16 Bits
- Vier-Byte-Code: 3 + 6 + 6 + 6 = 21 Bits
- Damit ist jeder ASCII-Text automatisch auch ein korrekt codierter UTF-8-Text.
- Sollten die bisher für Unicode festgelegten 17 Ebenen = 21 Bit = 1.1 Mill. mögliche Zeichen mal erweitert werden, dann wird UTF-8 auf 5- und 6-Byte-Codes erweitert.
## Zeichen und Zeichenketten in Julia
### Zeichen: `Char`
Der Datentyp `Char` kodiert ein einzelnes Unicode-Zeichen.
- Julia verwendet dafür einfache Anführungszeichen: `'a'`.
- Ein `Char` belegt 4 Bytes Speicher und
- repräsentiert einen Unicode-Codepoint.
- `Char`s können von/zu `UInt`s umgewandelt werden und
- der Integer-Wert ist gleich dem Unicode-codepoint.
### Zeichenketten: `String`
- Für Strings verwendet Julia doppelte Anführungszeichen: `"a"`.
- Sie sind UTF-8-codiert, d.h., ein Zeichen kann zwischen 1 und 4 Bytes lang sein.
```{julia}
@show typeof('a') sizeof('a') typeof("a") sizeof("a");
```
- `Char`s können von/zu `UInt`s umgewandelt werden.
```{julia}
UInt('a')
```
```{julia}
b = Char(0x2656)
```
__Bei einem Nicht-ASCII-String unterscheiden sich Anzahl der Bytes und Anzahl der Zeichen:__
```{julia}
asciistr = "Hello World!"
@show length(asciistr) ncodeunits(asciistr);
```
(Das Leerzeichen zählt natürlich auch.)
```{julia}
str = "😄 Hellö 🎶"
@show length(str) ncodeunits(str);
```
__Iteration über einen String iteriert über die Zeichen:__
```{julia}
for i in str
println(i, " ", typeof(i))
end
```
### Verkettung von Strings
"Strings mit Verkettung bilden ein nichtkommutatives Monoid."
Deshalb wird in Julia die Verkettung multiplikativ geschrieben.
```{julia}
str * asciistr * str
```
Damit sind auch Potenzen mit natürlichem Exponenten definiert.
```{julia}
str^3, str^0
```
### Stringinterpolation
Das Dollarzeichen hat in Strings eine Sonderfunktion, die wir schon oft in
`print()`-Anweisungen genutzt haben. MAn kann damit eine Variable oder einen Ausdruck interpolieren:
```{julia}
a = 33.4
b = "x"
s = "Das Ergebnis für $b ist gleich $a und die verdoppelte Wurzel daraus ist $(2sqrt(a))\n"
```
### Backslash escape sequences
Der _backslash_ `\` hat in Stringkonstanten ebenfalls eine Sonderfunktion.
Julia benutzt die von C und anderen Sprachen bekannten _backslash_-Codierungen für Sonderzeichen und für Dollarzeichen und Backslash selbst:
```{julia}
s = "So bekommt man \'Anführungszeichen\" und ein \$-Zeichen und einen\nZeilenumbruch und ein \\ usw... "
print(s)
```
### Triple-Quotes
Man kann Strings auch mit Triple-Quotes begrenzen.
In dieser Form bleiben Zeilenumbrüche und Anführungszeichen erhalten:
```{julia}
s = """
Das soll
ein "längerer"
'Text' sein.
"""
print(s)
```
### Raw strings
In einem `raw string` sind alle backslash-Codierungen außer `\"` abgeschaltet:
```{julia}
s = raw"Ein $ und ein \ und zwei \\ und ein 'bla'..."
print(s)
```
## Weitere Funktionen für Zeichen und Strings (Auswahl)
### Tests für Zeichen
```{julia}
@show isdigit('0') isletter('Ψ') isascii('\U2655') islowercase('α')
@show isnumeric('½') iscntrl('\n') ispunct(';');
```
### Anwendung auf Strings
Diese Tests lassen sich z.B. mit `all()`, `any()` oder `count()` auf Strings anwenden:
```{julia}
all(ispunct, ";.:")
```
```{julia}
any(isdigit, "Es ist 3 Uhr! 🕒" )
```
```{julia}
count(islowercase, "Hello, du!!")
```
### Weitere String-Funktionen
```{julia}
@show startswith("Lampenschirm", "Lamp") occursin("pensch", "Lampenschirm")
@show endswith("Lampenschirm", "irm");
```
```{julia}
@show uppercase("Eis") lowercase("Eis") titlecase("eiSen");
```
```{julia}
# remove newline from end of string
@show chomp("Eis\n") chomp("Eis");
```
```{julia}
split("π ist irrational.")
```
```{julia}
replace("π ist irrational.", "ist" => "ist angeblich")
```
## Indizierung von Strings
Strings sind nicht mutierbar aber indizierbar. Dabei gibt es ein paar Besonderheiten.
- Der Index nummeriert die Bytes des Strings.
- Bei einem nicht-ASCII-String sind nicht alle Indizes gültig, denn
- ein gültiger Index adressiert immer ein Unicode-Zeichen.
Unser Beispielstring:
```{julia}
str
```
Das erste Zeichen
```{julia}
str[1]
```
Dieses Zeichen ist in UTF8-Kodierung 4 Bytes lang. Damit sind 2,3 und 4 ungültige Indizes.
```{julia}
str[2]
```
Erst das 5. Byte ist ein neues Zeichen:
```{julia}
str[5]
```
Auch bei der Adressierung von Substrings müssen Anfang und Ende jeweils gültige Indizes sein, d.h., der Endindex muss ebenfalls das erste Byte eines Zeichens indizieren und dieses Zeichen ist das letzte des Teilstrings.
```{julia}
str[1:7]
```
Die Funktion `eachindex()` liefert einen Iterator über die gültigen Indizes:
```{julia}
for i in eachindex(str)
c = str[i]
println("$i: $c")
end
```
Wie üblich macht collect() aus einem Iterator einen Vektor.
```{julia}
collect(eachindex(str))
```
Die Funktion `nextind()` liefert den nächsten gültigen Index.
```{julia}
@show nextind(str, 1) nextind(str, 2);
```
Warum verwendet Julia einen Byte-Index und keinen Zeichenindex? Der Hauptgrund dürfte die Effizienz der Indizierung sein.
- In einem langen String, z.B. einem Buchtext, ist die Stelle `s[123455]` mit einem Byte-Index schnell zu finden.
- Ein Zeichen-Index müsste in der UTF-8-Codierung den ganzen String durchlaufen, um das n-te Zeichen zu finden, da die Zeichen 1,2,3 oder 4 Bytes lang sein können.
Einige Funktionen liefern Indizes oder Ranges als Resultat. Sie liefern immer gültige Indizes:
```{julia}
findfirst('l', str)
```
```{julia}
findfirst("Hel", str)
```
```{julia}
str2 = "αβγδϵ"^3
```
```{julia}
n = findfirst('γ', str2)
```
So kann man ab dem nächsten nach `n=5` gültigen Index weitersuchen:
```{julia}
findnext('γ', str2, nextind(str2, n))
```

314
chapters/5_TricksHelp.qmd Normal file
View File

@ -0,0 +1,314 @@
# Arbeit mit Julia: Hilfe, REPL, Pakete, Introspection
@sec-tab
## Dokumentation
Die offizielle Julia-Dokumentation [https://docs.julialang.org/](https://docs.julialang.org/) enthält zahlreiche Übersichten, darunter:
- [https://docs.julialang.org/en/v1/base/punctuation/](https://docs.julialang.org/en/v1/base/punctuation/) Verzeichnis der Symbole
- [https://docs.julialang.org/en/v1/manual/unicode-input/](https://docs.julialang.org/en/v1/manual/unicode-input/) Verzeichnis spezieller Unicode-Symbole und deren Eingabe in Julia via Tab-Vervollständigung
- [https://docs.julialang.org/en/v1/manual/mathematical-operations/#Rounding-functions](https://docs.julialang.org/en/v1/manual/mathematical-operations/#Rounding-functions) Liste mathematischer Funktionen
## Julia REPL (Read - Eval - Print - Loop)
Nach dem Start von Julia in einem Terminal kann man neben Julia-Code auch verschiedene Kommandos eingeben
:::{.narrow}
| Kommando | Wirkung |
| :----------------------------| :------------------------ |
| `exit()` oder `Ctrl-d` | exit Julia |
| `Ctrl-c` | interrupt |
| `Ctrl-l` | clear screen |
| Kommando mit `;` beenden | Ausgabe unterdrückt |
| `include("filename.jl")` | Datei mit Julia-Code einlesen und ausführen |
Der REPL hat verschiedene Modi:
| Modus | Prompt | Modus starten | Modus verlassen |
| :- | :- | :- | :- |
| default| `julia>` | | `Ctrl-d`|
| Package manager | `pkg>` | `]` | `backspace` |
| Help | `help?>` | `?`| `backspace `|
|Shell | `shell>` | `;` | `backspace`|
:::
## Jupyter-Notebooks (IJulia)
In einem Jupyter-Notebook sind die Modi sind als Einzeiler in einer eigenen Input-Zelle nutzbar:
(i) ein Kommando des Paket-Managers:
```{julia}
#| eval: false
] status
```
(ii) eine Help-Abfrage:
```{julia}
#| eval: false
?sin
```
(iii) Ein Shell-Kommando:
```{julia}
#| eval: false
;ls
```
## Der Paketmanager
- Über 9000 Pakete, siehe [https://julialang.org/packages/](https://julialang.org/packages/)
- Bevor sie mit `using Module` verwendet werden können, müssen sie heruntergeladen und installiert werden.
- Dazu dient der _package manager_ `Pkg`
- Man kann ihn auf zwei Arten verwenden:
- als normale Julia-Anweisungen, die auch in einer `.jl`-Programmdatei stehen können:
```
using Pkg
Pkg.add("TollesPaket")
```
- im speziellen pkg-Modus des Julia-REPLs:
```
] add TollesPaket
```
### Einige Funktionen des Paketmanagers
| Funktion | `pkg` - Mode | Erklärung |
|:------------------------|:----------------------|:----------------------------------------------------------|
| `Pkg.add("MyPack")` | `pkg> add MyPack` | add `MyPack.jl` to current environment |
| `Pkg.rm("MyPack")` | `pkg> remove MyPack` | remove `MyPack.jl` from current environment |
| `Pkg.update()` | `pkg> update` | update packages in current environment |
| `Pkg.activate("mydir")` | `pkg> activate mydir` | activate directory as current environment |
| `Pkg.status()` | `pkg> status` | list packages |
### Installierte Pakete und Environments
- Julia und der Paketmanager verwalten
1. eine Liste der mit dem Kommando `Pkg.add()` bzw. `]add` explizit installierten Pakete mit genauer Versionsbezeichnung in einer Datei `Project.toml` und
2. eine Liste aller dabei auch als implizite Abhängigkeiten installierten Pakete in der Datei `Manifest.toml`.
- Das Verzeichnis, in dem diese Dateien stehen, ist das `environment` und wird mit `Pkg.status()` bzw. `]status` angezeigt.
- Im Normalfall sieht das so aus:
```
(@v1.8) pkg> status
Status `~/.julia/environments/v1.8/Project.toml`
[1dea7af3] OrdinaryDiffEq v6.7.1
[91a5bcdd] Plots v1.27.1
[438e738f] PyCall v1.93.1
```
- Man kann für verschiedene Projekte eigene `environments` benutzen. Dazu kann man entweder Julia mit
```shell
julia --project=path/to/myproject
```
starten oder in Julia das environment mit `Pkg.activate("path/to/myproject")` aktivieren. Dann werden `Project.toml, Manifest.toml` dort angelegt und verwaltet. (Die Installation der Paketdateien erfolgt weiterhin irgendwo unter `$HOME/.julia`)
### Zum Installieren von Paketen auf unserem Jupyter-Server `misun103`:
- Es gibt ein zentrales Repository, in dem alle in diesem Kurs erwähnten Pakete bereits installiert sind.
- Dort haben Sie keine Schreibrechte.
- Sie können aber zusätzliche Pakete in Ihrem `HOME` installieren. Dazu ist als erster Befehl nötig, das aktuelle Verzeichnis zu aktivieren:
```{julia}
#| eval: false
] activate .
```
(Man beachte den Punkt!)
Danach können Sie mit `add` im Pkg-Modus auch Pakete installieren:
```{julia}
#| eval: false
] add TollesPaket
```
Achtung! Das kann dauern! Viele Pakete haben komplexe Abhängigkeiten und lösen die Installation von weiteren Paketen aus. Viele Pakete werden beim Installieren vorkompiliert. Im REPL sieht man den Installationsfortschritt, im Jupyter-Notebook nicht.
## Eingebaute Hilfe und Informationen
Mit `?` und der `Tab`-Taste kommt man oft weiter.
## Hilfe und praktische Tipps
----
### weitere Hilfe: `@which`, `fieldnames()`, `methods()`, `names()`, `pwd()`
```{julia}
# Die Zuordnung zu einem Modul zeigt @which an:
@which(sqrt)
```
```{julia}
# Die Komponenten einer struct oder eines anderen zusammengesetzten Typs:
fieldnames(Rational)
```
```{julia}
fieldnames(Complex)
```
```{julia}
# alle Methoden einer Funktion
methods(sqrt)
```
```{julia}
# alle Methoden einer Funktion bei bestimmten Argumenttypen. Die Argumenttypen müssen als Tupel angegeben werden
methods(sqrt, (Number,)) # Komma nötig für 1-elementiges Tupel
```
```{julia}
# für einen Typ gibt methods() alle Konstruktoren aus
methods(Int64)
```
```{julia}
# names(Module) gibt alle von einem Modul exportierte Namen aus.
# genau wie auch
#
# ?Module
#
# funktioniert es erst, wenn das Modul mit 'using Module' geladen ist.
# Oft ist es besser, wenn die using-Anweisung in einer eigenen Zelle steht.
using Plots
```
```{julia}
names(Plots)
```
```{julia}
# Julia kürzt den interaktiven Output.
# ein explizites print() zeigt alles:
print(names(Plots))
```
```{julia}
# eine andere Möglichkeit der Ausgabe mit Überlänge in Jupyter
show(stdout, MIME("text/plain"), names(Plots))
```
```{julia}
# pwd() "print working directory" zeigt, in welchem Verzeichnis Julia gerade operiert.
#
# Wichtig für die Ein/Ausgabe mit Dateien, z.B. include()
pwd()
```
```{julia}
include("Jupyter.jl") # Datei mit Julia-Code einlesen und abarbeiten
```
## Der Julia JIT _(just in time)_ Compiler
Julia baut auf die Werkzeuge des _LLVM Compiler Infrastructure Projects_ auf.
:::{.narrow}
Stages of Compilation
| stage & result | introspection command |
| :--- | :--- |
|Parse $\Longrightarrow$ Abstract Syntax Tree (AST) | `Meta.parse()` |
| Lowering: transform AST $\Longrightarrow$ Static Single Assignment (SSA) form | `@code_lowered`|
| Type Inference | `@code_warntype`, `@code_typed` |
| Generate LLVM intermediate representation | `@code_llvm`|
| Generate native machine code | `@code_native` |
:::
```{julia}
function f(x,y)
z = x^2 + log(y)
return 2z
end
```
```{julia}
p = Meta.parse( "function f(x,y); z=x^2+log(y); return 2x; end ")
```
```{julia}
using TreeView
walk_tree(p)
```
```{julia}
@code_lowered f(2,4)
```
```{julia}
@code_warntype f(2,4)
```
```{julia}
@code_typed f(2,4)
```
```{julia}
@code_llvm f(2,4)
```
```{julia}
@code_native f(2,4)
```

336
chapters/6_ArraysEtcP1.qmd Normal file
View File

@ -0,0 +1,336 @@
# Container
Julia bietet eine große Auswahl von Containertypen mit weitgehend ähnlichem Interface an.
Wir stellen hier `Tuple`, `Range` und `Dict` vor, im nächsten Kapitel dann `Array`, `Vector` und `Matrix`.
Diese Container sind:
- **iterierbar:** Man kann über die Elemente des Containers iterieren:
```julia
for x ∈ container ... end
```
- **indizierbar:** Man kann auf Elemente über ihren Index zugreifen:
```julia
x = container[i]
```
und einige sind auch
- **mutierbar**: Man kann Elemente hinzufügen, entfernen und ändern.
Weiterhin gibt es eine Reihe gemeinsamer Funktionen, z.B.
- `length(container)` --- Anzahl der Elemente
- `eltype(container)` --- Typ der Elemente
- `isempty(container)` --- Test, ob Container leer ist
- `empty!(container)` --- leert Container (nur wenn mutierbar)
## Tupeln
Ein Tupel ist ein nicht mutierbarer Container von Elementen. Es ist also nicht möglich, neue Elemente dazuzufügen oder den Wert eines Elements zu ändern.
```{julia}
t = (33, 4.5, "Hello")
@show t[2] # indizierbar
for i ∈ t println(i) end # iterierbar
```
Ein Tupel ist ein **inhomogener** Typ. Jedes Element hat seinen eigenen Typ und das zeigt sich auch im Typ des Tupels:
```{julia}
typeof(t)
```
Man verwendet Tupel gerne als Rückgabewerte von Funktionen, um mehr als ein Objekt zurückzulieferen.
```{julia}
# Ganzzahldivision und Rest:
# Quotient und Rest werden den Variablen q und r zugewiesen
q, r = divrem(71, 6)
@show q r;
```
Wie man hier sieht, kann man in bestimmten Konstrukten die Klammern auch weglassen.
Dieses *implict tuple packing/unpacking* verwendet man auch gerne in Mehrfachzuweisungen:
```{julia}
x, y, z = 12, 17, 203
```
```{julia}
y
```
Manche Funktionen bestehen auf Tupeln als Argument oder geben immer Tupeln zurück. Dann braucht man manchmal ein Tupel aus einem Element.
Das notiert man so:
```{julia}
x = (13,) # ein 1-Element-Tupel
```
Das Komma - und nicht die Klammern -- macht das Tupel.
```{julia}
x= (13) # kein Tupel
```
## Ranges
Wir haben *range*-Objekte schon in numerischen `for`-Schleifen verwendet.
```{julia}
r = 1:1000
typeof(r)
```
Es gibt verschiedene *range*-Typen. Wie man sieht, sind es über den Zahlentyp parametrisierte Typen und `UnitRange` ist z.B. ein *range* mit der Schrittweite 1. Ihre Konstruktoren heißen in der Regel `range()`.
Der Doppelpunkt ist eine spezielle Syntax.
- `a:b` wird vom Parser umgesetzt zu `range(a, b)`
- `a:b:c` wird umgesetzt zu `range(a, c, step=b)`
*Ranges* sind offensichtlich iterierbar, nicht mutierbar aber indizierbar.
```{julia}
(3:100)[20] # das zwanzigste Element
```
Wir erinnern an die Semantik der `for`-Schleife: `for i in 1:1000` heißt **nicht**:
- 'Die Schleifenvariable `i` wird bei jedem Durchlauf um eins erhöht' **sondern**
- 'Der Schleifenvariable werden nacheinander die Werte 1,2,3,...,1000 aus dem Container zugewiesen'.
Allerdings wäre es sehr ineffektiv, diesen Container tatsächlich explizit anzulegen.
- _Ranges_ sind "lazy" Vektoren, die nie wirklich irgendwo als konkrete Liste abgespeichert werden. Das macht sie als Iteratoren in `for`-Schleifen so nützlich: speichersparend und schnell.
- Sie sind 'Rezepte' oder Generatoren, die auf die Abfrage 'Gib mir dein nächstes Element!' antworten.
- Tatsächlich ist der Muttertyp `AbstractRange` ein Subtyp von `AbstractVector`.
Das Macro `@allocated` gibt aus, wieviel Bytes an Speicher bei der Auswertung eines Ausdrucks alloziert wurden.
```{julia}
@allocated [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]
```
```{julia}
@allocated 1:20
```
Zum Umwandeln in einen 'richtigen' Vektor dient die Funktion `collect()`.
```{julia}
collect(20:-3:1)
```
Recht nützlich, z.B. beim Vorbereiten von Daten zum Plotten, ist der *range*-Typ `LinRange`.
```{julia}
LinRange(2, 50, 300)
```
`LinRange(start, stop, n)` erzeugt eine äquidistante Liste von `n` Werten von denen der erste und der letzte die vorgegebenen Grenzen sind.
Mit `collect()` kann man bei Bedarf auch daraus den entsprechenden Vektor gewinnen.
## Dictionaries
- _Dictionaries_ (deutsch: "assoziative Liste" oder "Zuordnungstabelle" oder ...) sind spezielle Container.
- Einträge in einem Vektor `v` sind durch einen Index 1,2,3.... addressierbar: `v[i]`
- Einträge in einem _dictionary_ sind durch allgemeinere _keys_ addressierbar.
- Ein _dictionary_ ist eine Ansammlung von _key-value_-Paaren.
- Damit haben _dictionaries_ in Julia den parametrisierten Typ `Dict{S,T}`, wobei `S` der Typ der _keys_ und `T` der Typ der _values_ ist
Man kann sie explizit anlegen:
```{julia}
# Einwohner 2020 in Millionen, Quelle: wikipedia
EW = Dict("Berlin" => 3.66, "Hamburg" => 1.85,
"München" => 1.49, "Köln" => 1.08)
```
```{julia}
typeof(EW)
```
und mit den _keys_ indizieren:
```{julia}
EW["Berlin"]
```
Das Abfragen eines nicht existierenden _keys_ ist natürlich ein Fehler.
```{julia}
EW["Leipzig"]
```
Man kann ja auch vorher mal anfragen...
```{julia}
haskey(EW, "Leipzig")
```
... oder die Funktion `get(dict, key, default)` benutzen, die bei nicht existierendem Key keinen Fehler wirft sondern das 3. Argument zurückgibt.
```{julia}
@show get(EW, "Leipzig", -1) get(EW, "Berlin", -1);
```
Man kann sich auch alle `keys` und `values` als spezielle Container geben lassen.
```{julia}
keys(EW)
```
```{julia}
values(EW)
```
Man kann über die `keys` iterieren...
```{julia}
for i in keys(EW)
n = EW[i]
println("Die Stadt $i hat $n Millionen Einwohner.")
end
```
odere gleich über `key-value`-Paare.
```{julia}
for (stadt, ew) ∈ EW
println("$stadt : $ew Mill.")
end
```
### Erweitern und Modifizieren
Man kann in ein `Dict` zusätzliche `key-value`-Paare eintragen...
```{julia}
EW["Leipzig"] = 0.52
EW["Dresden"] = 0.52
EW
```
und einen `value` ändern.
```{julia}
# Oh, das war bei Leipzig die Zahl von 2010, nicht 2020
EW["Leipzig"] = 0.597
EW
```
Ein Paar kann über seinen `key` auch gelöscht werden.
```{julia}
delete!(EW, "Dresden")
```
Zahlreiche Funktionen können mit `Dicts` wie mit anderen Containern arbeiten.
```{julia}
maximum(values(EW))
```
### Anlegen eines leeren Dictionaries
Ohne Typspezifikation ...
```{julia}
d1 = Dict()
```
und mit Typspezifikation:
```{julia}
d2 = Dict{String, Int}()
```
### Umwandlung in Vektoren: `collect()`
- `keys(dict)` und `values(dict)` sind spezielle Datentypen.
- Die Funktion `collect()` macht daraus eine Liste vom Typ `Vector`.
- `collect(dict)` liefert eine Liste vom Typ `Vector{Pair{S,T}}`
```{julia}
collect(EW)
```
```{julia}
collect(keys(EW)), collect(values(EW))
```
### Geordnetes Iterieren über ein Dictionary
Wir sortieren die Keys. Als Strings werden sie alphabetisch sortiert. Mit dem `rev`-Parameter wird rückwärts sortiert.
```{julia}
for k in sort(collect(keys(EW)), rev = true)
n = EW[k]
println("$k hat $n Millionen Einw. ")
end
```
Wir sortieren `collect(dict)`. Das ist ein Vektor von Paaren. Mit `by` definieren wir, wonach zu sortieren ist: nach dem 2. Element des Paares.
```{julia}
for (k,v) in sort(collect(EW), by = pair -> last(pair), rev=false)
println("$k hat $v Mill. EW")
end
```
### Eine Anwendung von Dictionaries: Zählen von Häufigkeiten
Wir machen 'experimentelle Stochastik' mit 2 Würfeln:
Gegeben sei `l`, eine Liste mit den Ergebnissen von 100 000 Pasch-Würfen, also 100 000 Zahlen zwischen 2 und 12.
Wie häufig sind die Zahlen 2 bis 12?
Wir (lassen) würfeln:
```{julia}
l = rand(1:6, 100_000) .+ rand(1:6, 100_000)
```
Wir zählen mit Hilfe eines Dictionaries die Häufigkeiten der Ereignisse. Dazu nehmen wir das Ereignis als `key` und seine Häufigkeit als `value`.
```{julia}
# In diesem Fall könnte man das auch mit einem einfachen Vektor
# lösen. Eine bessere Illustration wäre z.B. Worthäufigkeit in
# einem Text. Dann ist i keine ganze Zahl sondern ein Wort=String
d = Dict{Int,Int}() # das Dict zum 'reinzählen'
for i in l # für jedes i wird d[i] erhöht.
d[i] = get(d, i, 0) + 1
end
d
```
Das Ergebnis:
```{julia}
using Plots
plot(collect(keys(d)), collect(values(d)), seriestype=:scatter)
```
##### Das Erklär-Bild dazu:
[https://math.stackexchange.com/questions/1204396/why-is-the-sum-of-the-rolls-of-two-dices-a-binomial-distribution-what-is-define](https://math.stackexchange.com/questions/1204396/why-is-the-sum-of-the-rolls-of-two-dices-a-binomial-distribution-what-is-define)

928
chapters/7_ArraysP2.qmd Normal file
View File

@ -0,0 +1,928 @@
# Vektoren, Matrizen, Arrays
## Allgemeines
Kommen wir nun zu den wohl wichtigsten Containern für numerische Mathematik:
- Vektoren `Vector{T}`
- Matrizen `Matrix{T}` mit zwei Indizes
- N-dimensionale Arrays mit N Indizes `Array{T,N}`
Tatsächlich ist `Vector{T}` ein Alias für `Array{T,1}` und `Matrix{T}` ein Alias für `Array{T,2}`.
```{julia}
Vector{Float64} === Array{Float64,1} && Matrix{Float64} === Array{Float64,2}
```
Beim Anlegen durch eine explizite Elementliste wird der 'kleinste gemeinsame Typ' für den Typparameter `T` ermittelt.
```{julia}
v = [33, "33", 1.2]
```
Falls `T` ein numerischer Typ ist, werden die Elemente in diesen Typ umgewandelt.
```{julia}
v = [3//7, 4, 2im]
```
Informationen über einen Array liefern die Funktionen:
- `length(A)` --- Anzahl der Elemente
- `eltype(A)` --- Typ der Elemente
- `ndims(A)` --- Anzahl der Dimensionen (Indizes)
- `size(A)` --- Tupel mit den Dimensionen des Arrays
```{julia}
v1 = [12, 13, 15]
m1 = [ 1 2.5
6 -3 ]
for f ∈ (length, eltype, ndims, size)
println("$(f)(v) = $(f(v)), $(f)(m1) = $(f(m1))")
end
```
- Die Stärke des 'klassischen' Arrays für das wissenschaftliche Rechnen besteht darin, dass es einfach nur ein zusammenhängendes Speichersegment ist, in dem die Komponenten gleicher Länge (z.B. 64 Bit) geordnet hintereinander abgespeichert sind. Damit ist der Speicherbedarf minimal und die Zugriffsgeschwindigkeit auf eine Komponente, sowohl beim Lesen als auch beim Modifizieren, maximal. Der Platz der Komponente `v[i]` ist sofort aus `i` berechenbar.
- Julias `Array{T,N}` (und damit Vektoren und Matrizen) ist für die üblichen numerischen Typen `T` in dieser Weise implementiert. Die Elemente werden *unboxed* gespeichert. Im Gegensatz dazu ist z.B. ein `Vector{Any}` implementiert als Liste von Adressen von Objekten *(boxed)* und nicht als Liste der Objekte selbst.
- Julias `Array{T,N}` speichert seine Elemente direkt *(unboxed)*, wenn `isbitstype(T) == true`.
```{julia}
isbitstype(Float64),
isbitstype(Complex{Rational{Int64}}),
isbitstype(String)
```
## Vektoren
### Listen-artige Funktionen
- `push!(vector, items...)` --- fügt Elemente am Ende des Vektors an
- `pushfirst!(vector, items...)` --- fügt Elemente am Anfang des Vektors an
- `pop!(vector)` --- entfernt letztes Element und liefert es als Ergebnis zurück,
- `popfirst!(vector)` --- entfernt erstes Element und liefert es zurück
```{julia}
v = Float64[] # leerer Vector{Float64}
push!(v, 3, 7)
pushfirst!(v, 1)
a = pop!(v)
println("a= $a")
push!(v, 17)
```
Ein `push!()` kann sehr aufwändig sein, da eventuell neuer Speicher alloziert und dann der ganze bestehende Vektor umkopiert werden muss. Julia optimiert das Speichermanagement. Es wird in einem solchen Fall Speicher auf Vorrat alloziert, so dass weitere `push!`s sehr schnell sind und man 'fast O(1)-Geschwindigkeit' erreicht.
Trotzdem sollte man bei zeitkritischem Code und sehr großen Feldern Operationen wie `push!()` oder `resize()` vermeiden.
### Weitere Konstruktoren
Man kann Vektoren mit vorgegebener Länge und Typ uninitialisiert anlegen. Das geht am Schnellsten, die Elemente sind zufällige Bitmuster.
```{julia}
# fixe Länge 1000, uninitialisiert
v = Vector{Float64}(undef, 1000)
v[345]
```
- `zeros(n)` legt einen `Vector{Float64}` der Länge `n` an und initialisiert mit Null.
```{julia}
v = zeros(7)
```
- `zeros(T,n)` legt einen Nullvektor vom Typ `T` an.
```{julia}
v=zeros(Int, 4)
```
- `fill(x, n)` legt `Vector{typeof(x)}` der Länfe `n` an und füllt mit `x`.
```{julia}
v = fill(sqrt(2), 5)
```
- `similar(v)` legt einen uninitialisierten Vektor von gleichem Typ und Größe wie `v` an.
```{julia}
w = similar(v)
```
### Konstruktion durch implizite Schleife _(list comprehension)_
Implizite `for`-Schleifen sind eine weitere Methode, Vektoren zu erzeugen.
```{julia}
v4 = [i for i in 1.0:8]
```
```{julia}
v5 = [log(i^2) for i in 1:4 ]
```
Man kann sogar noch ein `if` unterbringen.
```{julia}
v6 = [i^2 for i in 1:8 if i%3 != 2]
```
### Bitvektoren {#sec-bitvec}
Neben `Vector{Bool}` gibt es noch den speziellen Datentyp `BitVector` (und allgemeiner auch `BitArray`) zur Speicherung von Feldern mit Wahrheitswerten.
Während für die Speicherung eines `Bool`s ein Byte verwendet wird, erfolgt die Speicherung in einem BitVector bitweise.
Der Konstruktor wandelt einen
`Vector{Bool}` in einen `BitVector` um.
```{julia}
vb = BitVector([true, false, true, true])
```
Für die Gegenrichtung gibt es `collect()`.
```{julia}
collect(vb)
```
BitVectoren entstehen z.B. als Ergebnis von elementweisen Vergleichen (s. @sec-broadcast).
```{julia}
v4 .> 3.5
```
### Indizierung
Für den Mathematiker sind Indizes Ordinalzahlen. Also __startet die Indexzählung mit 1.__
Als Index kann man verwenden:
- Integer
- Integer-wertigen Range (gleiche Länge oder kürzer)
- Integer-Vektor (gleiche Länge oder kürzer)
- Bool-Vektor oder BitVector (gleiche Länge)
Mit Indizes kann man Arrayelemente/teile lesen und schreiben.
```{julia}
v = [ 3i + 5.2 for i in 1:8]
```
```{julia}
v[5]
```
Bei Zuweisungen wird die rechte Seite wenn nötig mit `convert(T,x)` in den Vektorelementetyp umgewandelt.
```{julia}
v[6] = 9999
v
```
Überschreiten der Indexgrenzen führt zu einem `BoundsError`.
```{julia}
v[77]
```
Mit einem `range`-Objekt kann man einen Teilvektor adressieren.
```{julia}
vp = v[3:5]
vp
```
```{julia}
vp = v[1:2:7] # range mit Schrittweite
vp
```
- Bei der Verwendung als Index kann in einem Range der Spezialwert `end` verwendet werden.
- Bei der Verwendung als Index kann der "leere" Range `:` als Abkürzung von `1:end` verwendet werden. Das ist nützlich bei Matrizen: `A[:, 3]` adressiert die gesamte 3. Spalte von `A`.
```{julia}
v[6:end] = [7, 7, 7]
v
```
#### Indirekte Indizierung
Die indirekte Indizierung mit einem *Vector of Integers/Indices* erfolgt nach der Formel
$v[ [i_1,\ i_2,\ i_3,...]] = [\ v[i_1],\ v[i_2],\ v[i_3],...]$
```{julia}
v[ [1, 3, 4] ]
```
ist also gleich
```{julia}
[ v[1], v[3], v[4] ]
```
#### Indizierung mit einem Vektor von Wahrheitswerten
Als Index kann man auch einen `Vector{Bool}` oder `BitVector` (s. @sec-bitvec) **derselben Länge** verwenden.
```{julia}
v[ [true, true, false, false, true, false, true, true] ]
```
Das ist nützlich, da man z.B.
- Tests broadcasten kann (s. @sec-broadcast),
- diese Tests dann einen BitVector liefern und
- bei Bedarf solche Bitvektoren durch die Bit-weisen Operatoren `&` und `|` verknüpft werden können.
```{julia}
v[ (v .> 13) .& (v.<20) ]
```
## Matrizen und Arrays
Die bisher vorgestellten Methoden für Vektoren übertragen sich auch auf höherdimensionale Arrays.
Man kann sie uninitialisiert anlegen:
```{julia}
A = Array{Float64,3}(undef, 6,9,3)
```
In den meisten Funktionen kann man die Dimensionen auch als Tupel übergeben. Die obige Anweisung lässt sich auch so schreiben:
```julia
A = Array{Float64, 3}(undef, (6,9,3))
```
Funktionen wie `zeros()` usw. funktionieren natürlich auch.
```{julia}
m2 = zeros(3, 4, 2) # oder zeros((3,4,2))
```
```{julia}
M = fill(5 , (3, 3)) # oder fill(5, 3, 3)
```
Die Funktion `similar()`, die einen Array gleicher Größe uninitialisiert erzeugt, kann auch einen Typ als weiteres Argument bekommen.
```{julia}
M2 = similar(M, Float64)
```
### Konstruktion durch explizite Elementliste
Während man Vektoren kommagetrennt in eckigen Klammern notiert, ist die Notation für höherdimensionale Objekte etwas anders.
- Eine Matrix:
```{julia}
M2 = [2 3 -1
4 5 -2]
```
- dieselbe Matrix:
```{julia}
M2 = [2 3 -1; 4 5 -2]
```
- Ein Array mit 3 Indizes:
```{julia}
M3 = [2 3 -1
4 5 6 ;;;
7 8 9
11 12 13]
```
- und nochmal die Matrix `M2`:
```{julia}
M2 = [2;4;; 3;5;; -1;-2]
```
Im letzten Beispiel kommen diese Regeln zur Anwendung:
- Trenner ist das Semikolon.
- Ein Semikolon `;` erhöht den 1. Index.
- Zwei Semikolons `;;` erhöhen den 2. Index.
- Drei Semikolons `;;;` erhöhen den 3. Index usw.
In den Beispielen davor wurde folgende Syntaktische Verschönerung (_syntactic sugar_) angewendet:
- Leerzeichen trennen wie 2 Semikolons -- erhöht also den 2. Index: $\quad a_{12}\quad a_{13}\quad a_{14}\ ...$
- Zeilenumbruch trennt wie ein Semikolon -- erhöht also den 1. Index.
:::{.callout-important}
- Vektorschreibweise mit Komma als Trenner geht nur bei Vektoren, nicht mit "Semikolon, Leerzeichen, Newline" mischen!
- Vektoren, $1\!\times\!n$-Matrizen und $n\!\times\!1$-Matrizen sind drei verschiedene Dinge!
```{julia}
v1 = [2,3,4]
```
```{julia}
v2 = [2;3;4]
```
```{julia}
v3 = [2 3 4]
```
```{julia}
v3 = [2;3;4;;]
```
:::
Einen "vector of vectors" a la C/C++ kann man natürlich auch konstruieren.
```{julia}
v = [[2,3,4], [5,6,7,8]]
```
```{julia}
v[2][3]
```
Das sollte man nur in Spezialfällen tun. Die Array-Sprache von Julia ist in der Regel bequemer und schneller.
### Indizes, Teilfelder, Slices
```{julia}
# 6x6 Matrix mit Zufallszahlen gleichverteilt aus [0,1) ∈ Float64
A = rand(6,6)
```
Die übliche Indexnotation:
```{julia}
A[2, 3] = 77.77777
A
```
Man kann mit Ranges Teilfelder adressieren:
```{julia}
B = A[1:2, 1:3]
```
Das Adressieren von Teilen mit geringerer Dimension wird auch *slicing* genannt.
```{julia}
# die 3. Spalte als Vektor (slicing)
C = A[:, 3]
```
```{julia}
# die 3. Zeile als Vektor (slicing)
E = A[3, :]
```
Natürlich sind damit auch Zuweisungen möglich:
```{julia}
# Man kann slices und Teilfeldern auch etwas zuweisen
A[2, :] = [1,2,3,4,5,6]
A
```
## Verhalten bei Zuweisungen, `copy()` und `deepcopy()`, Views
### Zuweisungen und Kopien
- Variablen sind Referenzen auf Objekte.
- _Eine Zuweisung zu einer Variablen erzeugt kein neues Objekt._
```{julia}
A = [1, 2, 3]
B = A
```
`A` und `B` sind jetzt Namen desselben Objekts.
```{julia}
A[1] = 77
@show B;
```
```{julia}
B[3] = 300
@show A;
```
Dieses Verhalten spart viel Zeit und Speicher, ist aber nicht immer gewünscht.
Die Funktion `copy()` erzeugt eine 'echte' Kopie des Objekts.
```{julia}
A = [1, 2, 3]
B = copy(A)
A[1] = 100
@show A B;
```
Die Funktion
`deepcopy(A)` kopiert rekursiv. Auch von den Elementen, aus denen `A` besteht, werden (wieder rekursive) Kopien erstellt.
Solange ein Array nur primitive Objekte (Zahlen) enthält, sind `copy()` und `deepcopy()` äquivalent.
Das folgende Beispiel zeigt den Unterschied zwischen `copy()` und `deepcopy()`.
```{julia}
mutable struct Person
name :: String
age :: Int
end
A = [Person("Meier", 20), Person("Müller", 21), Person("Schmidt", 23)]
B = A
C = copy(A)
D = deepcopy(A)
```
```{julia}
A[1] = Person("Mustermann", 83)
A[3].age = 199
@show B C D;
```
### Views
Wenn man mittels *indices/ranges/slices* einer Variablen ein Teilstück eines Arrays zuweist,
wird von Julia grundsätzlich **ein neues Objekt** konstruiert.
```{julia}
A = [1 2 3
3 4 5]
v = A[:, 2]
@show v
A[1, 2] = 77
@show A v;
```
Manchmal möchte man aber gerade hier eine Referenz-Semantik haben im Sinne von: "Vektor `v` soll der 2. Spaltenvektor von `A` sein und auch bleiben (d.h., sich mitändern, wenn sich `A` ändert)."
Dies bezeichnet man in Julia als *views*: Wir wollen, dass die Variable `v` nur einen 'alternativen Blick' auf die Matrix `A` darstellt.
Das kann man erreichen durch das `@view`-Macro:
```{julia}
A = [1 2 3
3 4 5]
v = @view A[:,2]
@show v
A[1, 2] = 77
@show v;
```
Diese Technik wird von Julia aus Effizienzgründen auch bei einigen Funktionen der linearen Algebra verwendet.
Ein Beispiel ist der Operator `'`, der zu einer Matrix `A` die adjungierte Matrix `A'` liefert.
- Die adjungierte _(adjoint)_ Matrix `A'` ist die transponierte und elementweise komplex-konjugierte Matrix zu `A`.
- Der Parser macht daraus den Funktionsaufruf `adjoint(A)`.
- Für reelle Matrizen ist die Adjungierte gleich der transponierten Matrix.
- Julia implementiert `adjoint()` als _lazy function_, d.h.,
- es wird aus Effizienzgründen kein neues Objekt konstruiert, sondern nur ein alternativer 'View' auf die Matrix ("mit vertauschten Indizes") und ein alternativer 'View' auf die Einträge (mit Vorzeichenwechsel im Imaginärteil).
- Aus Vektoren macht `adjoint()` eine $1\!\times\!n$-Matrix (einen Zeilenvektor).
```{julia}
A = [ 1. 2.
3. 4.]
B = A'
```
Die Matrix `B` ist nur ein modifizierter 'View' auf `A`:
```{julia}
A[1, 2] =10
B
```
Aus Vektoren macht `adjoint()` eine $1\!\times\!n$-Matrix (einen Zeilenvektor).
```{julia}
v = [1, 2, 3]
v'
```
Eine weitere solche Funktion, die einen alternativen 'View', eine andere Indizierung, derselben Daten
liefert, ist `reshape()`.
Hier wird ein Vektor mit 12 Einträgen in eine 3x4-Matrix verwandelt.
```{julia}
A = [1,2,3,4,5,6,7,8,9,10,11,12]
B = reshape(A, 3, 4)
```
## Speicherung eines Arrays
- Speicher wird linear adressiert. Eine Matrix kann zeilenweise _(row major)_ oder spaltenweise _(column major)_ im Speicher angeordnet sein.
- C/C++/Python(NumPy) verwenden eine zeilenweise Speicherung: Die 4 Elemente einer 2x2-Matrix sind abgespeichert in der Reihenfolge $a_{11},a_{12},a_{21},a_{22}$.
- Julia, Fortran, Matlab speichern spaltenweise: $a_{11},a_{21},a_{12},a_{22}$.
Diese Information ist wichtig, um effizient über Matrizen zu iterieren:
```{julia}
function column_major_add(A, B)
(n,m) = size(A)
for j = 1:m
for i = 1:n # innere Schleife durchläuft eine Spalte
A[i,j] += B[i,j]
end
end
end
function row_major_add(A, B)
(n,m) = size(A)
for i = 1:n
for j = 1:m # inere Schleife durchläuft eine Zeile
A[i,j] += B[i,j]
end
end
end
```
```{julia}
A = rand(10000, 10000);
B = rand(10000, 10000);
```
```{julia}
using BenchmarkTools
@benchmark row_major_add($A, $B)
```
```{julia}
@benchmark column_major_add($A, $B)
```
### Lokalität von Speicherzugriffen und _Caching_
Wir haben gesehen, dass die Reihenfolge von innerem und äußerem Loop einen erheblichen Geschwindigkeitsunterschied macht:
__Es ist effizienter, wenn die innerste Schleife über den linken Index läuft__, also eine Spalte und nicht eine Zeile durchläuft. Die Ursache dafür liegt in der Architektur moderner Prozessoren.
- Speicherzugriffe erfolgt über mehrere Cache-Ebenen.
- Ein _cache miss_, der ein Nachladen aus langsameren Caches auslöst, bremst aus.
- Es werden immer gleich größere Speicherblöcke nachgeladen, um die Häufigkeit von _cache misses_ zu minimieren.
- Daher ist es wichtig, Speicherzugriffe möglichst lokal zu organisieren.
::: {.content-visible when-format="html"}
![Speicherhierarchie von Intel-Prozessoren, aus: Victor Eijkhout,_Introduction to High-Performance Scientific Computing_, [https://theartofhpc.com/](https://theartofhpc.com/)](../images/cache.png){width="75%"}
:::
::: {.content-visible when-format="pdf"}
![Speicherhierarchie von Intel-Prozessoren, aus: Victor Eijkhout,_Introduction to High-Performance Scientific Computing_, [https://theartofhpc.com/](https://theartofhpc.com/)](../images/cache.png){width="70%"}
:::
## Mathematische Operationen mit Arrays
Arrays der gleichen Dimension (z.B. alle $7\!\times\!3$-Matrizen) bilden einen linearen Raum.
- Sie können mit Skalaren multipliziert werden und
- sie können addiert und subtrahiert werden.
```{julia}
0.5 * [2, 3, 4, 5]
```
```{julia}
0.5 * [ 1 3
2 7] - [ 2 3; 1 2]
```
### Matrixprodukt
Das Matrixprodukt ist definiert für
:::{.narrow}
| 1. Faktor | 2. Faktor | Produkt |
| :-: | :-: | :-: |
| $(n\!\times\!m)$-Matrix | $(m\!\times\!k)$-Matrix | $(n\times k)$-Matrix|
| $(n\!\times\!m)$-Matrix | $m$-Vektor | $n$-Vektor |
| $(1\!\times\!m)$-Zeilenvektor | $(m\!\times\!n)$-Matrix | $n$-Vektor |
| $(1\!\times\!m)$-Zeilenvektor | $m$-Vektor | Skalarprodukt |
| $m$-Vektor | $(1\times n)$-Zeilenvektor | $(m\!\times\!n)$-Matrix |
:::
Beispiele:
```{julia}
A = [1 2 3
4 5 6]
v = [2, 3]
w = [1, 3, 4];
```
- (2,3)-Matrix `*` (3,2)-Matrix
```{julia}
A * A'
```
- (3,2)-Matrix `*` (2,3)-Matrix
```{julia}
A' * A
```
- (2,3)-Matrix `*` 3-Vektor
```{julia}
A * w
```
- (1,2)-Vektore `*` (2,3)-Matrix
```{julia}
v' * A
```
- (3,2)-Matrix `*` 2-Vektor
```{julia}
A' * v
```
- (1,2)-Vektor `*` 2-Vektor (Skalarprodukt)
```{julia}
v' * v
```
2-Vektor `*` (1,3)-Vektor (äußeres Produkt)
```{julia}
v * w'
```
## Broadcasting {#sec-broadcast}
- Beim _broadcasting_ werden Operationen oder Funktionen __elementweise__ auf Arrays angewendet.
- Die Syntax dafür ist ein Punkt _vor_ einem Operationszeichen oder _nach_ einem Funktionsnamen.
- Der Parser setzt `f.(x,y)` um zu `broadcast(f, x, y)` und analog für Operatoren `x .⊙ y` zu `broadcast(⊙, z, y)`.
- Dabei werden Operanden, denen eine oder mehrere Dimensionen fehlen, in diesen Dimensionen (virtuell) vervielfältigt.
- Das *broadcasting* von Zuweisungen `.=`, `.+=`,... verändert die Semantik. Es wird kein neues Objekt erzeugt, sondern die Werte werden in das links stehende Objekt (welches die richtige Dimension haben muss) eingetragen.
Einige Beispiele:
- Elementweise Anwendung einer Funktion
```{julia}
sin.([1, 2, 3])
```
- Das Folgende liefert nicht die algebraische [Wurzel aus einer Matrix](https://de.wikipedia.org/wiki/Quadratwurzel_einer_Matrix), sondern die elementweise Wurzel aus jedem Eintrag.
```{julia}
A = [8 2
3 4]
sqrt.(A)
```
- Das Folgende liefert nicht $A^2$, sondern die Einträge werden quadriert.
```{julia}
A.^2
```
- Zum Vergleich das Ergebnis der algebraischen Operationen:
```{julia}
@show A^2 A^(1/2);
```
- Broadcasting geht auch mit Funktionen mehrerer Variablen.
```{julia}
hyp(a,b) = sqrt(a^2+b^2)
B = [3 4
5 7]
hyp.(A, B)
```
Bei Operanden verschiedener Dimension wird der Operand mit fehlenden Dimensionen in diesen durch Vervielfältigung virtuell
'aufgeblasen'.
Wir addieren einen Skalar zu einer Matrix:
```{julia}
A = [ 1 2 3
4 5 6]
```
```{julia}
A .+ 300
```
Der Skalar wurde durch Replikation auf dieselbe Dimension wie die Matrix gebracht. Wir lassen uns von `broadcast()` die Form des 2. Operanden nach dem *broadcasting* anzeigen:
```{julia}
broadcast( (x,y) -> y, A, 300)
```
(Natürlich findet diese Replikation nur virtuell statt. Dieses Objekt wird bei anderen Operationen nicht wirklich erzeugt.)
Als weiteres Beispiel: Matrix und (Spalten-)Vektor
```{julia}
A .+ [10, 20]
```
Der Vektor wird durch Wiederholung der Spalten aufgeblasen:
```{julia}
broadcast((x,y)->y, A, [10,20])
```
Matrix und Zeilenvektor: Der Zeilenvektor wird zeilenweise vervielfältigt:
```{julia}
A .* [1,2,3]' # Adjungierter Vektor
```
Der 2. Operand wird von `broadcast()` durch Vervielfältigung der Zeilen 'aufgeblasen'.
```{julia}
broadcast((x,y)->y, A, [1,2,3]')
```
#### _Broadcasting_ bei Zuweisungen
Zuweisungen `=`, `+=`, `/=`,..., bei denen links ein Name steht, laufen in Julia so ab, dass aus der rechten Seite ein Objekt konstruiert und diesem Objekt der neue Name zugewiesen wird.
Beim Arbeiten mit Arrays will man allerdings sehr oft aus Effizienzgründen einen bestehenden Array weiterverwenden. Die rechts berechneten Einträge sollen in das bereits existierende Objekt auf der linken Seite eingetragen werden.
Das erreicht man mit den Broadcast-Varianten `.=`, `.+=`,... der Zuweisungsoperatoren.
```{julia}
A .= 3
```
```{julia}
A .+= [1, 4]
```
## Weitere Array-Funktionen - eine Auswahl
Julia stellt eine große Anzahl von Funktionen bereit, die mit Arrays arbeiten.
```{julia}
A = [22 -17 8 ; 4 6 9]
```
- Finde das Maximum
```{julia}
maximum(A)
```
- Finde das Maximum jeder Spalte
```{julia}
maximum(A, dims=1)
```
- Finde das Maximum jeder Zeile
```{julia}
maximum(A, dims=2)
```
- Finde das Minimum und seine Position
```{julia}
amin, i = findmin(A)
```
- Was ist ein `CartesianIndex`?
```{julia}
dump(i)
```
- Extrahiere die Indizes des Minimum als Tupel
```{julia}
i.I
```
- Summe und Produkt aller Einträge
```{julia}
sum(A), prod(A)
```
- Spaltensumme (1. Index wird reduziert)
```{julia}
sum(A, dims=1)
```
- Zeilensummen (2. Index wird reduziert)
```{julia}
sum(A, dims=2)
```
- Summiere nach elementweiser Anwendung einer Funktion
```{julia}
sum(x->sqrt(abs(x)), A) # sum_ij sqrt(|a_ij|)
```
- Reduziere (falte) den Array mit einer Funktion
```{julia}
reduce(+, A) # equivalent to sum(A)
```
- `mapreduce(f, op, array)`: Wende `f` auf alle Einträge an, dann reduziere mit `op`
```{julia}
mapreduce(x -> x^2, +, A ) # Summe der Quadrate aller Einträge
```
- Gibt es Elemente in A, die > 5 sind?
```{julia}
any(x -> x>5, A)
```
- Wieviele Elemente in A sind > 5?
```{julia}
count(x-> x>5, A)
```
- sind alle Einträge positiv?
```{julia}
all(x-> x>0, A)
```

625
chapters/9_functs.qmd Normal file
View File

@ -0,0 +1,625 @@
# Funktionen und Operatoren
Funktionen verarbeiten ihre Argumente zu einem Ergebnis, das sie beim Aufruf zurückliefern.
## Formen
Funktionen können in verschiedenen Formen definiert werden:
I. Als `function ... end`-Block
```{julia}
function hyp(x,y)
sqrt(x^2+y^2)
end
```
II. Als "Einzeiler"
```{julia}
hyp(x, y) = sqrt(x^2 + y^2)
```
III. Als anonyme Funktionen
```{julia}
(x, y) -> sqrt(x^2 + y^2)
```
### Block-Form und `return`
- Mit `return` wird die Abarbeitung der Funktion beendet und zum aufrufenden Kontext zurückgekehrt.
- Ohne `return` wird der Wert des letzten Ausdrucks als Funktionswert zurückgegeben.
Die beiden Definitionen
```julia
function xsinrecipx(x)
if x == 0
return 0.0
end
return x * sin(1/x)
end
```
und ohne das zweite explizite `return` in der letzten Zeile:
```julia
function xsinrecipx(x)
if x == 0
return 0.0
end
x * sin(1/x)
end
```
sind also äquivalent.
- Eine Funktion, die "nichts" zurückgibt (_void functions_ in der C-Welt), gibt den Wert `nothing` vom Typ `Nothing` zurück. (So wie ein Objekt vom Typ `Bool` die beiden Werte `true` und `false` haben kann, so kann ein Objekt vom Typ `Nothing` nur einen einzigen Wert, eben `nothing`, annehmen.)
- Eine leere `return`-Anweisung ist äquivalent zu `return nothing`.
```{julia}
function fn(x)
println(x)
return
end
a = fn(2)
```
```{julia}
a
```
```{julia}
@show a typeof(a);
```
### Einzeiler-Form
Die Einzeilerform ist eine ganz normale Zuweisung, bei der links eine Funktion steht.
```julia
hyp(x, y) = sqrt(x^2 + y^2)
```
Julia kennt zwei Möglichkeiten, mehrere Anweisungen zu einem Block zusammenzufassen, der an Stelle einer Einzelanweisung stehen kann:
- `begin ... end`-Block
- Eingeklammerte durch Semikolon getrennte Anweisungen.
In beiden Fällen ist der Wert des Blockes der Wert der letzten Anweisung.
Damit funktioniert auch
```julia
hyp(x, y) = (z = x^2; z += y^2; sqrt(z))
```
und
```julia
hyp(x, y) = begin
z = x^2
z += y^2
sqrt(z)
end
```
### Anonyme Funktionen
Anonyme FUnktionen kann man der Anonymität entreisen, indem man ihnen einen Namen zuweist.
```julia
hyp = (x,y) -> sqrt(x^2 + y^2)
```
Ihre eigentliche Anwendung ist aber im Aufruf einer *(higher order)* Funktion, die eine Funktion als Argument erwartet.
Typische Anwendungen sind `map(f, collection)`, welches eine Funktion auf alle Elemente einer Kollektion anwendet. In Julia funktioniert auch `map(f(x,y), collection1, collection2)`:
```{julia}
map( (x,y) -> sqrt(x^2 + y^2), [3, 5, 8], [4, 12, 15])
```
```{julia}
map( x->3x^3, 1:8 )
```
Ein weiteres Beispiel ist `filter(test, collection)`, wobei ein Test eine Funktion ist, die ein `Bool` zurückgibt.
```{julia}
filter(x -> ( x%3 == 0 && x%5 == 0), 1:100 )
```
## Argumentübergabe
- Beim Funktionsaufruf werden von den als Funktionsargumente zu übergebenden Objekten keine Kopien gemacht. Die Variablen in der Funktion verweisen auf die Originalobjekte. Julia nennt dieses Konzept _pass_by_sharing_.
- Funktionen können also ihre Argumente wirksam modifizieren, falls es sich um _mutable_ Objekte, wie z.B. `Vector`, `Array` handelt.
- Es ist eine Konvention in Julia, dass die Namen von solchen argumentmodifizierenden Funktionen mit einem Ausrufungszeichen enden. Weiterhin steht dann üblicherweise das Argument, das modifiziert wird, an erster Stelle und es ist auch der Rückgabewert der Funktion.
```{julia}
V = [1, 2, 3]
W = fill!(V, 17)
# '===' ist Test auf Identität
@show V W V===W; # V und W benennen dasselbe Objekt
```
```{julia}
function fill_first!(V, x)
V[1] = x
return V
end
U = fill_first!(V, 42)
@show V U V===U;
```
## Varianten von Funktionsargumenten
- Es gibt Positionsargumente (1. Argument, 2. Argument, ....) und _keyword_-Argumente, die beim Aufruf durch ihren Namen angesprochen werden müssen.
- Sowohl Positions- als auch _keyword_-Argumente können _default_-Werte haben. Beim Aufruf können diese Argumente weggelassen werden.
- Die Reihenfolge der Deklaration muss sein:
1. Positionsargumente ohne Defaultwert,
2. Positionsargumente mit Defaultwert,
3. --- Semikolon ---,
4. kommagetrennte Liste der Keywordargumente (mit oder ohne Defaultwert)
- Beim Aufruf können _keyword_-Argumente an beliebigen Stellen in beliebiger Reihenfolge stehen. Man kann sie wieder durch ein Semikolon von den Positionsargumenten abtrennen, muss aber nicht.
```{julia}
fa(x, y=42; a) = println("x=$x, y=$y, a=$a")
fa(6, a=4, 7)
fa(6, 7; a=4)
fa(a=-2, 6)
```
Eine Funktion nur mit _keyword_-Argumenten wird so deklariert:
```{julia}
fkw(; x=10, y) = println("x=$x, y=$y")
fkw(y=2)
```
## Funktionen sind ganz normale Objekte
- Sie können zugewiesen werden
```{julia}
f2 = sqrt
f2(2)
```
- Sie können als Argumente an Funktionen übergeben werden.
```{julia}
# sehr naive numerische Integration
function Riemann_integrate(f, a, b; NInter=1000)
delta = (b-a)/NInter
s = 0
for i in 0:NInter-1
s += delta * f(a + delta/2 + i * delta)
end
return s
end
Riemann_integrate(sin, 0, π)
```
- Sie können von Funktionen erzeugt und als Ergebnis `return`t werden.
```{julia}
function generate_add_func(x)
function addx(y)
return x+y
end
return addx
end
```
```{julia}
h = generate_add_func(4)
```
```{julia}
h(1)
```
```{julia}
h(2), h(10)
```
Die obige Funktion `generate_add_func()` lässt sich auch kürzer definieren. Der innere Funktionsname `addx()` ist sowieso lokal und außerhalb nicht verfügbar. Also kann man eine anonyme Funktion verwenden.
```{julia}
generate_add_func(x) = y -> x + y
```
## Zusammensetzung von Funktionen: die Operatoren $\circ$ und `|>`
- Die Zusammensetzung _(composition)_ von Funktionen kann auch mit dem Operator $\circ$ (`\circ + Tab`) geschrieben werden
$$(f\circ g)(x) = f(g(x))$$
```{julia}
(sqrt ∘ + )(9, 16)
```
```{julia}
f = cos ∘ sin ∘ (x->2x)
f(.2)
```
```{julia}
@show map(uppercase ∘ first, ["ein", "paar", "grüne", "Blätter"]);
```
- Es gibt auch einen Operator, mit dem Funktionen "von rechts" wirken und zusammengesetzt werden können _(piping)_
```{julia}
25 |> sqrt
```
```{julia}
1:10 |> sum |> sqrt
```
- Natürlich kann man auch diese Operatoren 'broadcasten' (s. @sec-broadcast). Hier wirkt ein Vektor von Funktionen elementweise auf einen Vektor von Argumenten:
```{julia}
["a", "list", "of", "strings"] .|> [length, uppercase, reverse, titlecase]
```
## Die `do`-Notation
Eine syntaktische Besonderheit zur Definition anonymer Funktionen als Argumente anderer Funktionen ist die `do`-Notation.
Sei `higherfunc(f,a,...)` eine Funktion, deren 1. Argument eine Funktion ist.
Dann kann man `higherfunc()` auch ohne erstes Argument aufrufen und statt dessen die Funktion in einem unmittelbar folgenden `do`-Block definieren:
```julia
higherfunc(a, b) do x, y
Körper von f(x,y)
end
```
Am Beispiel von `Riemann_integrate()` sieht das so aus:
```{julia}
# das ist dasselbe wie Riemann_integrate(x->x^2, 0, 2)
Riemann_integrate(0, 2) do x x^2 end
```
Der Sinn besteht natürlich in der Anwendung mit komplexeren Funktionen, wie diesem aus zwei Teilstücken zusammengesetzten Integranden:
```{julia}
r = Riemann_integrate(0, π) do x
z1 = sin(x)
z2 = log(1+x)
if x > 1
return z1^2
else
return 1/z2^2
end
end
```
## Funktionsartige Objekte
Durch Definition einer geeigneten Methode für einen Typ kann man beliebige Objekte *callable* machen, d.h., sie anschließend wie Funktionen aufrufen.
```{julia}
# struct speichert die Koeffiziente eines Polynoms 2. Grades
struct Poly2Grad
a0::Float64
a1::Float64
a2::Float64
end
p1 = Poly2Grad(2,5,1)
p2 = Poly2Grad(3,1,-0.4)
```
Die folgende Methode macht diese Struktur `callable`.
```{julia}
function (p::Poly2Grad)(x)
p.a2 * x^2 + p.a1 * x + p.a0
end
```
Jetzt kann man die Objekte, wenn gewünscht, auch wie Funktionen verwenden.
```{julia}
@show p2(5) p1(-0.7) p1;
```
## Operatoren und spezielle Formen
- Infix-Operatoren wie `+,*,>,∈,...` sind Funktionen.
```{julia}
+(3, 7)
```
```{julia}
f = +
```
```{julia}
f(3, 7)
```
- Auch Konstruktionen wie `x[i]`, `a.x`, `[x; y]` werden vom Parser zu Funktionsaufrufen umgewandelt.
:::{.narrow}
| | |
| :-: | :------------ |
| x[i] | getindex(x, i) |
| x[i] = z | setindex!(x, z, i) |
| a.x | getproperty(a, :x) |
| a.x = z | setproperty!(a, :x, z) |
| [x; y;...] | vcat(x, y, ...) |
:Spezielle Formen [(Auswahl)](https://docs.julialang.org/en/v1/manual/functions/#Operators-With-Special-Names)
:::
(Der Doppelpunkt vor einer Variablen macht diese zu einem Symbol.)
:::{.callout-note}
Für diese Funktionen kann man eigene Methoden implementieren. Zum Beispiel könnten bei einem eigenen Typ das Setzen eines Feldes (`setproperty!()`) die Gültigkeit des Wertes prüfen oder weitere Aktionen veranlassen.
Prinzipiell können `get/setproperty` auch Dinge tun, die gar nichts mit einem tatsächlich vorhandenen Feld der Struktur zu tun haben.
:::
## Update-Form
Alle arithmetischen Infix-Operatoren haben eine update-Form: Der Ausdruck
```julia
x = x ⊙ y
```
kann auch geschrieben werden als
```julia
x ⊙= y
```
Beide Formen sind semantisch äquivalent. Insbesondere wird in beiden Formen der Variablen `x` ein auf der rechten Seite geschaffenes neues Objekt zugewiesen.
Ein Speicherplatz- und Zeit-sparendes __in-place-update__ eines Arrays/Vektors/Matrix ist möglich entweder durch explizite Indizierung
```julia
for i in eachindex(x)
x[i] += y[i]
end
```
oder durch die dazu semantisch äquivalente _broadcast_-Form (s. @sec-broadcast):
```julia
x .= x .+ y
```
## Vorrang und Assoziativität von Operatoren {#sec-vorrang}
Zu berechnende Ausdrücke
```{julia}
-2^3+500/2/10==8 && 13 > 7 + 1 || 9 < 2
```
werden vom Parser in eine Baumstruktur überführt.
```{julia}
using TreeView
walk_tree(Meta.parse("-2^3+500/2/10==8 && 13 > 7 + 1 || 9 < 2"))
```
- Die Auswertung solcher Ausdrücke wird durch
- Vorrang _(precedence)_ und
- Assoziativität geregelt.
- 'Vorrang' definiert, welche Operatoren stärker binden im Sinne von "Punktrechnung geht vor Strichrechnung".
- 'Assoziativität' bestimmt die Auswertungsreihenfolge bei gleichen oder gleichrangigen Operatoren.
- [Vollständige Dokumentation](https://docs.julialang.org/en/v1/manual/mathematical-operations/#Operator-Precedence-and-Associativity)
### Assoziativität
Sowohl Addition und Subtraktion als auch Multiplikation und Divison sind jeweils gleichrangig und linksassoziativ, d.h. es wird von links ausgewertet.
```{julia}
200/5/2 # wird von links ausgewertet als (200/5)/2
```
```{julia}
200/2*5 # wird von links ausgewertet als (200/2)*5
```
Zuweisungen wie `=`, `+=`, `*=`,... sind gleichrangig und rechtsassoziativ.
```{julia}
x = 1
y = 10
# wird von rechts ausgewertet: x += (y += (z = (a = 20)))
x += y += z = a = 20
@show x y z a;
```
Natürlich kann man die Assoziativität in Julia auch abfragen. Die entsprechenden Funktionen werden nicht expizit aus dem `Base`-Modul exportiert, deshalb muss man den Modulnamen beim Aufruf angeben.
```{julia}
for i in (:/, :+=, :(=), :^)
a = Base.operator_associativity(i)
println("Operation $i is $(a)-assoziative")
end
```
Also ist der Potenzoperator rechtsassoziativ.
```{julia}
2^3^2 # rechtsassoziativ, = 2^(3^2)
```
### Vorrang
- Julia ordnet den Operatoren Vorrangstufen von 1 bis 17 zu:
```{julia}
for i in (:+, :-, :*, :/, :^, :(=))
p = Base.operator_precedence(i)
println("Vorrang von $i = $p")
end
```
- 11 ist kleiner als 12, also geht 'Punktrechnung vor Strichrechnung'
- Der Potenz-Operator `^` hat eine höhere _precedence_.
- Zuweisungen haben die kleinste _precedence_
```{julia}
# Zuweisung hat kleinsten Vorrang, daher Auswertung als x = (3 < 4)
x = 3 < 4
x
```
```{julia}
(y = 3) < 4 # Klammern schlagen natürlich jeden Vorrang
y
```
Nochmal zum Beispiel vom Anfang von @sec-vorrang:
```{julia}
-2^3+500/2/10==8 && 13 > 7 + 1 || 9 < 2
```
```{julia}
for i ∈ (:^, :+, :/, :(==), :&&, :>, :|| )
print(i, " ")
println(Base.operator_precedence(i))
end
```
Nach diesen Vorrangregeln wird der Beispielausdruck also wie folgt ausgewertet:
```{julia}
((-(2^3)+((500/2)/10)==8) && (13 > (7 + 1))) || (9 < 2)
```
(Das entspricht natürlich dem oben gezeigten *parse-tree*)
Es gilt also für den Vorrang:
> Potenz > Multiplikation/Division > Addition/Subtraktion > Vergleiche > logisches && > logisches || > Zuweisung
Damit wird ein Ausdruck wie
```julia
a = x <= y + z && x > z/2
```
sinnvoll ausgewertet als `a = ((x <= (y+z)) && (x < (z/2)))`
- Eine Besonderheit sind noch
- unäre Operatoren, also insbesondere `+` und `-` als Vorzeichen
- _juxtaposition_, also Zahlen direkt vor Variablen oder Klammern ohne `*`-Symbol
Beide haben Vorrang noch vor Multiplikation und Division.
:::{.callout-important}
Damit ändert sich die Bedeutung von Ausdrücken, wenn man _juxtaposition_ anwendet:
```{julia}
1/2*π, 1/2π
```
:::
- Im Vergleich zum Potenzoperator `^` gilt (s. [https://discourse.julialang.org/t/confused-about-operator-precedence-for-2-3x/8214/7](https://discourse.julialang.org/t/confused-about-operator-precedence-for-2-3x/8214/7) ):
> Unary operators, including juxtaposition, bind tighter than ^ on the right but looser on the left.
Beispiele:
```{julia}
-2^2 # -(2^2)
```
```{julia}
x = 5
2x^2 # 2(x^2)
```
```{julia}
2^-2 # 2^(-2)
```
```{julia}
2^2x # 2^(2x)
```
- Funktionsanwendung `f(...)` hat Vorrang vor allen Operatoren
```{julia}
sin(x)^2 === (sin(x))^2 # nicht sin(x^2)
```
### Zusätzliche Operatoren
Der [Julia-Parser](https://github.com/JuliaLang/julia/blob/master/src/julia-parser.scm#L13-L31) definiert für zahlreiche Unicode-Zeichen einen Vorrang auf Vorrat, so dass diese Zeichen von Paketen und selbstgeschriebenem Code als Operatoren benutzt werden können.
So haben z.B.
```julia
∧ ⊗ ⊘ ⊙ ⊚ ⊛ ⊠ ⊡ ⊓ ∙ ∤ ⅋ ≀ ⊼ ⋄ ⋆ ⋇ ⋉ ⋊ ⋋ ⋌ ⋏ ⋒ ⟑ ⦸ ⦼ ⦾ ⦿ ⧶ ⧷ ⨇ ⨰ ⨱ ⨲ ⨳ ⨴ ⨵ ⨶ ⨷ ⨸ ⨻ ⨼ ⨽ ⩀ ⩃ ⩄ ⩋ ⩍ ⩎ ⩑ ⩓ ⩕ ⩘ ⩚ ⩜ ⩞ ⩟ ⩠ ⫛
```
den Vorrang 12 wie Multiplikation/Division (und sind wie diese linksassoziativ)
und z.B.
```julia
⊕ ⊖ ⊞ ⊟ |++| ⊔ ± ∓ ∔ ∸ ≏ ⊎ ⊻ ⊽ ⋎ ⋓ ⧺ ⧻ ⨈ ⨢ ⨣ ⨤ ⨥ ⨦ ⨧ ⨨ ⨩ ⨪ ⨫ ⨬ ⨭ ⨮ ⨹ ⨺ ⩁ ⩂ ⩅ ⩊ ⩌ ⩏ ⩐ ⩒ ⩔ ⩖ ⩗
```
haben den Vorrang 11 wie Addition/Subtraktion.

223
chapters/Erste_Bsp.qmd Normal file
View File

@ -0,0 +1,223 @@
# Erste Miniprogramme
## Was sollte man zum Programmieren können?
- **Denken in Algorithmen:**
Welche Schritte sind zur Lösung des Problems nötig? Welche Daten müssen gespeichert, welche Datenstrukturen angelegt werden? Welche Fälle können auftreten und müssen erkannt und behandelt werden?
- **Umsetzung des Algorithmus in ein Programm:**
Welche Datenstrukturen, Konstrukte, Funktionen... stellt die Programmiersprache bereit?
- **Formale Syntax:**
Menschen verstehen 'broken English'; Computer verstehen kein 'broken C++' oder 'broken Julia'. Die Syntax muss beherrscht werden.
- **„Ökosystem“ der Sprache:**
Wie führe ich meinen Code aus? Wie funktionieren die Entwicklungsumgebungen? Welche Optionen versteht der Compiler? Wie installiere ich Zusatzbibliotheken? Wie lese ich Fehlermeldungen? Wo kann ich mich informieren?
- **die Kunst des Debugging:**
Programmieranfänger sind oft glücklich, wenn sie alle Syntaxfehler eliminiert haben und das Programm endlich 'durchläuft'. Bei komplexeren Programmen fängt die Arbeit jetzt erst an, denn nun muss getestet und die Fehler im Algorithmus gefunden und behoben werden.
- **Gefühl für die Effizienz und Komplexität von Algorithmen**
- **Besonderheiten der Computerarithmetik**, insbesondere der Gleitkommazahlen
Das erschließt sich nicht alles auf einmal. Haben Sie Geduld mit sich und 'spielen' Sie mit der Sprache.
Das Folgende soll eine kleine Anregung dazu sein.
## Project Euler
Eine hübsche Quelle für Programmieraufgaben mit mathematischem Charakter und sehr unterschiedlichen Schwierigkeitsgraden ist [Project Euler](https://projecteuler.net/).
Die Aufgaben sind so gestaltet, dass keinerlei Eingaben notwendig und das gesuchte Ergebnis eine einzige Zahl ist. So kann man sich voll auf das Programmieren des Algorithmus konzentrieren.
---------
### Beispiel 1
::::::{.content-hidden unless-format="xxx"}
https://github.com/luckytoilet/projecteuler-solutions/blob/master/Solutions.md
https://math.stackexchange.com/questions/3370978/iterating-sum-of-squares-of-decimal-digits-of-an-integer-how-big-can-it-grow
aufg. 92
:::
::: {.callout-note icon=false .titlenormal}
## Project Euler Problem No 1
If we list all the natural numbers below 10 that are multiples of 3 or 5, we get 3, 5, 6 and 9. The sum of these multiples is 23.
Find the sum of all the multiples of 3 or 5 below 1000.
:::
1. Algorithmus:
- Teste alle natürlichen Zahlen <1000 auf Teilbarkeit durch 3 oder durch 5 und
- summiere die teilbaren Zahlen auf.
2. Umsetzung:
Wie liefert Julia den Rest der Ganzzahldivision? Solche Funktionen heißen typischerweise `rem()` (für *remainder*) oder `mod()`. [Nachlesen in der Doku](https://docs.julialang.org/en/v1/base/math/#Base.rem) oder ausprobieren von `?rem` und `?mod` in der Julia-REPL zeigt, dass es beides gibt. Der Unterschied liegt in der Behandlung negativer ganzer Zahlen. Für natürliche Zahlen `n,m` liefern `mod(n,m)` und `rem(n,m)` dasselbe und letzteres hat auch noch die Infix-Form `n % m`.
Wie testet man eine Reihe von Werten durch und summiert auf? Da gibt es ein Standardmuster: `for`-Schleife und Akkumulatorvariable:
:::{.callout-caution icon="false" collapse="true" .titlenormal}
## Eine Lösungsvariante:
```{julia}
s = 0 # Akkumulatorvariable initialisieren
for i in 1:999 # Schleife
if i % 3 == 0 || i % 5 == 0 # Test
s += i # Zu Akkumulator addieren
end # Ende Test
end # Ende Schleife
println(" Die Antwort ist: $s") # Ausgabe des Ergebnisses
```
:::
Natürlich geht das auch etwas kürzer:
:::{.callout-caution icon="false" collapse="true" .titlenormal}
## Noch eine Lösungsvariante:
```{julia}
sum([i for i in 1:999 if i%3==0||i%5==0])
```
:::
-----------
### Beispiel 2
::: {.callout-note icon=false .titlenormal}
## Project Euler Problem No 92
A number chain is created by continuously adding the square of the digits in a number to form a new number until it has been seen before.
For example,
| 44 → 32 → 13 → 10 → **1** → **1**
| 85 → **89** → 145 → 42 → 20 → 4 → 16 → 37 → 58 → **89**
Therefore any chain that arrives at 1 or 89 will become stuck in an endless loop. What is most amazing is that EVERY starting number will eventually arrive at 1 or 89.
How many starting numbers below ten million will arrive at 89?
:::
Programme kann man [*top-down* und *bottom-up*](https://de.wikipedia.org/wiki/Top-Down-_und_Bottom-Up-Design) entwickeln.
*Top-down* würde hier wohl bedeuten: „Wir fangen mit einer Schleife `for i = 1:9999999` an.“ Der andere Weg führt über einzeln testbare Komponenten und ist oft zielführender. (Und ab einer gewissen Projektgröße nähert man sich sowieso von 'top' und 'bottom' gleichzeitig dem Ziel.)
##### Funktion Nr. 1
Es soll untersucht werden, wie sich die Zahlen unter dem wiederholten Berechnen der 'Quadratquersumme' (Summe der Quadrate der Ziffern) verhalten. Also sollte man eine Funktion schreiben und testen, die diese 'Quadratquersumme' berechnet.
:::{.callout-caution icon="false" collapse="true" .titlenormal}
## `q2sum(n)` berechnet die Summe der Quadrate der Ziffern von n im Dezimalsystem:
```{julia}
#| output: false
function q2sum(n)
s = 0 # Akkumulator für die Summe
while n > 9 # solange noch mehrstellig...
q, r = divrem(n, 10) # berechne Ganzzahlquotient und Rest bei Division durch 10
s += r^2 # addiere quadrierte Ziffer zum Akkumulator
n = q # mache weiter mit der durch 10 geteilten Zahl
end
s += n^2 # addiere das Quadrat der letzten Ziffer
return s
end
```
:::
... und das testen wir jetzt natürlich:
```{julia}
for i in [1, 7, 33, 111, 321, 1000, 73201]
j = q2sum(i)
println("Quadratquersumme von $i = $j")
end
```
Im Sinne der Aufgabe wenden wir die Funktion wiederholt an:
```{julia}
n = 129956799
for i in 1:14
n = q2sum(n)
println(n)
end
```
... und haben hier einen der '89er Zyklen' getroffen.
#### Funktion Nr. 2
Wir müssen testen, ob die wiederholte Anwendung der Funktion `q2sum()` schließlich zu **1** führt oder in diesem **89er**-Zyklus endet.
:::{.callout-caution icon="false" collapse="true" .titlenormal}
## `q2test(n)` ermittelt, ob wiederholte Quadratquersummenbildung in den 89er-Zyklus führt:
```{julia}
#| output: false
function q2test(n)
while n !=1 && n != 89 # solange wir nicht bei 1 oder 89 angekommen sind....
n = q2sum(n) # wiederholen
end
if n==1 return false end # keine 89er-Zahl
return true # 89er-Zahl gefunden
end
```
:::
... und damit können wir die Aufgabe lösen:
```{julia}
c = 0 # mal wieder ein Akkumulator
for i = 1 : 10_000_000 - 1 # so kann man in Julia Tausenderblöcke zur besseren Lesbarkeit abtrennen
if q2test(i) # q2test() gibt einen Boolean zurück, kein weiterer Test nötig
c += 1 # 'if x == true' ist redundant, 'if x' reicht völlig
end
end
println("$c numbers below ten million arrive at 89.")
```
Zahlen, bei denen die iterierte Quadratquersummenbildung bei 1 endet, heißen übrigens [fröhliche Zahlen](https://de.wikipedia.org/wiki/Fr%C3%B6hliche_Zahl) und wir haben gerade die Anzahl der traurigen Zahlen kleiner als 10.000.000 berechnet.
Hier nochmal das vollständige Programm:
:::{.callout-caution icon="false" collapse="true" .titlenormal}
## unsere Lösung von Project Euler No 92:
```{julia}
function q2sum(n)
s = 0
while n > 9
q, r = divrem(n, 10)
s += r^2
n = q
end
s += n^2
return s
end
function q2test(n)
while n !=1 && n != 89
n = q2sum(n)
end
if n==1 return false end
return true
end
c = 0
for i = 1 : 10_000_000 - 1
if q2test(i)
c += 1
end
end
println("$c numbers below ten million arrive at 89.")
```
:::

225
chapters/Pi2.qmd Normal file
View File

@ -0,0 +1,225 @@
---
format:
html:
include-in-header:
text: |
<script type="application/javascript">
window.PlotlyConfig = {MathJaxConfig: 'local'}
</script>
---
# Ein Beispiel zur Stabilität von Gleitkommaarithmetik
## Berechnung von $\pi$ nach Archimedes
```{julia}
#| error: false
#| echo: false
#| output: false
using PlotlyJS, Random
using HypertextLiteral
using JSON, UUIDs
using Base64
## see https://github.com/JuliaPlots/PlotlyJS.jl/blob/master/src/PlotlyJS.jl
## https://discourse.julialang.org/t/encode-a-plot-to-base64/27765/3
function IJulia.display_dict(p::PlotlyJS.SyncPlot)
Dict(
# "application/vnd.plotly.v1+json" => JSON.lower(p),
# "text/plain" => sprint(show, "text/plain", p),
"text/html" => let
buf = IOBuffer()
show(buf, MIME("text/html"), p)
#close(buf)
#String(resize!(buf.data, buf.size))
String(take!(buf))
end,
"image/png" => let
buf = IOBuffer()
buf64 = Base64EncodePipe(buf)
show(buf64, MIME("image/png"), p)
close(buf64)
#String(resize!(buf.data, buf.size))
String(take!(buf))
end,
)
end
function Base.show(io::IO, mimetype::MIME"text/html", p::PlotlyJS.SyncPlot)
uuid = string(UUIDs.uuid4())
show(io,mimetype,@htl("""
<div style="height: auto" id=\"$(uuid)\"></div>
<script>
require(['../js/plotly-latest.min.js'], function(plotly) {
plotly.newPlot($(uuid),
$(HypertextLiteral.JavaScript(json(p.plot.data))),
$(HypertextLiteral.JavaScript(json(p.plot.layout))),{responsive: true});
});
</script>
"""))
end
```
Eine untere Schranke für $2\pi$, den Umfang des Einheitskreises, erhält man durch die
Summe der Seitenlängen eines dem Einheitskreis eingeschriebenen regelmäßigen $n$-Ecks.
Die Abbildung links zeigt, wie man beginnend mit einem Viereck der Seitenlänge $s_4=\sqrt{2}$ die Eckenzahl iterativ verdoppelt.
:::{.narrow}
| Abb. 1 | Abb.2 |
| :-: | :-: |
| ![](../images/pi1.png) | ![](../images/pi2.png) |
: {tbl-colwidths="[57,43]"}
:::
Die zweite Abbildung zeigt die Geometrie der Eckenverdoppelung.
Mit
$|\overline{AC}|= s_{n},\quad |\overline{AB}|= |\overline{BC}|= s_{2n},\quad |\overline{MN}| =a, \quad |\overline{NB}| =1-a,$ liefert Pythagoras für die Dreiecke $MNA$ und
$NBA$ jeweils
$$
a^2 + \left(\frac{s_{n}}{2}\right)^2 = 1\qquad\text{bzw.} \qquad
(1-a)^2 + \left(\frac{s_{n}}{2}\right)^2 = s_{2n}^2
$$
Elimination von $a$ liefert die Rekursion
$$
s_{2n} = \sqrt{2-\sqrt{4-s_n^2}} \qquad\text{mit Startwert}\qquad
s_4=\sqrt{2}
$$
für die Länge $s_n$ **einer** Seite des eingeschriebenen regelmäßigen
$n$-Ecks.
Die Folge $(n\cdot s_n)$
konvergiert monoton von unten gegen den
Grenzwert $2\pi$:
$$
n\, s_n \rightarrow 2\pi % \qquad\text{und} \qquad {n c_n}\downarrow 2\pi
$$
Der relative Fehler der Approximation von 2π durch ein $n$-Eck ist:
$$
\epsilon_n = \left| \frac{n\, s_n-2 \pi}{2\pi} \right|
$$
## Zwei Iterationsvorschriften^[nach: Christoph Überhuber, „Computer-Numerik“ Bd. 1, Springer 1995, Kap. 2.3]
Die Gleichung
$$
s_{2n} = \sqrt{2-\sqrt{4-s_n^2}}\qquad \qquad \textrm{(Iteration A)}
$$
ist mathematisch äquivalent zu
$$
s_{2n} = \frac{s_n}{\sqrt{2+\sqrt{4-s_n^2}}} \qquad \qquad \textrm{(Iteration B)}
$$
(Bitte nachrechnen!)
Allerdings ist Iteration A schlecht konditioniert und numerisch instabil, wie der folgende Code zeigt. Ausgegeben wird die jeweils berechnete Näherung für π.
```{julia}
using Printf
iterationA(s) = sqrt(2 - sqrt(4 - s^2))
iterationB(s) = s / sqrt(2 + sqrt(4 - s^2))
s_B = s_A = sqrt(2) # Startwert
ϵ(x) = abs(x - 2π)/2π # rel. Fehler
ϵ_A = Float64[] # Vektoren für den Plot
ϵ_B = Float64[]
is = Float64[]
@printf """ approx. Wert von π
n-Eck Iteration A Iteration B
"""
for i in 3:35
push!(is, i)
s_A = iterationA(s_A)
s_B = iterationB(s_B)
doublePi_A = 2^i * s_A
doublePi_B = 2^i * s_B
push!(ϵ_A, ϵ(doublePi_A))
push!(ϵ_B, ϵ(doublePi_B))
@printf "%14i %20.15f %20.15f\n" 2^i doublePi_A/2 doublePi_B/2
end
```
Während Iteration B sich stabilisiert bei einem innerhalb der Maschinengenauigkeit korrekten Wert für π, wird Iteration A schnell instabil. Ein Plot der relativen Fehler $\epsilon_i$ bestätigt das:
```{julia}
using PlotlyJS
layout = Layout(xaxis_title="Iterationsschritte", yaxis_title="rel. Fehler",
yaxis_type="log", yaxis_exponentformat="power",
xaxis_tick0=2, xaxis_dtick=2)
plot([scatter(x=is, y=ϵ_A, mode="markers+lines", name="Iteration A", yscale=:log10),
scatter(x=is, y=ϵ_B, mode="markers+lines", name="Iteration B", yscale=:log10)],
layout)
```
## Stabilität und Auslöschung
Bei $i=26$ erreicht Iteration B einen relativen Fehler in der Größe des Maschinenepsilons:
```{julia}
ϵ_B[22:28]
```
Weitere Iterationen verbessern das Ergebnis nicht mehr. Sie stabilisieren sich bei einem relativen Fehler von etwa 2.5 Maschinenepsilon:
```{julia}
ϵ_B[end]/eps(Float64)
```
Die Form Iteration A ist instabil. Bereits bei $i=16$ beginnt der relative Fehler wieder zu wachsen.
Ursache ist eine typische Auslöschung. Die Seitenlängen $s_n$ werden sehr schnell klein. Damit ist
$a_n=\sqrt{4-s_n^2}$ nur noch wenig kleiner als 2 und bei der Berechnung von $s_{2n}=\sqrt{2-a_n}$ tritt ein typischer Auslöschungseffekt auf.
```{julia}
setprecision(80) # precision für BigFloat
s = sqrt(BigFloat(2))
@printf " a = √(4-s^2) als BigFloat und als Float64\n\n"
for i = 3:44
s = iterationA(s)
x = sqrt(4-s^2)
if i > 20
@printf "%i %30.26f %20.16f \n" i x Float64(x)
end
end
```
Man sieht die Abnahme der Zahl der signifikanten Ziffern. Man sieht auch, dass eine Verwendung von `BigFloat` mit einer Mantissenlänge von hier 80 Bit das Einsetzen des Auslöschungseffekts nur etwas hinaussschieben kann.
**Gegen instabile Algorithmen helfen in der Regel nur bessere, stabile Algorithmen und nicht genauere Maschinenzahlen!**
:::{.content-hidden unless-format="xxx"}
Offensichtlich tritt bei der Berechnung von $2-a_n$ bereits relativ früh
eine Abnahme der Anzahl der signifikanten Ziffern (Auslöschung) auf,
bevor schließlich bei der Berechnung von $a_n=\sqrt{4-s_n^2}$
selbst schon Auslöschung zu einem unbrauchbaren Ergebnis führt.
:::

View File

@ -0,0 +1,148 @@
# Entwicklungsumgebungen
Für diesen Kurs werden wir die webbasierte Entwicklungsumgebung [Jupyterhub](https://de.wikipedia.org/wiki/Project_Jupyter) verwenden. Sie steht allen Teilnehmerïnnen für die Dauer des Kurses zur Verfügung.
Für langfristiges Arbeiten empfiehlt sich eine eigene Installation.
## Installation auf eigenem Rechner (Linux/MacOS/MS Windows)
1. Julia mit dem Installations- und Update-Manager **juliaup** installieren: <https://github.com/JuliaLang/juliaup/>
2. als Editor/IDE **Visual Studio Code** installieren: <https://code.visualstudio.com/>
3. im VS Code Editor die **Julia language extension** installieren: <https://www.julia-vscode.org/docs/stable/gettingstarted/>
Einstieg:
- In VS Code eine neue Datei mit der Endung `.jl` anlegen
- Julia-Code schreiben
- `Shift-Enter` oder `Ctrl-Enter` am Ende einer Anweisung oder eines Blocks startet eine Julia-REPL, Code wird in die REPL kopiert und ausgeführt
- [Tastenbelegungen für VS Code](https://code.visualstudio.com/docs/getstarted/keybindings#_keyboard-shortcuts-reference)
und [für Julia in VS Code](https://www.julia-vscode.org/docs/stable/userguide/keybindings/)
### Die Julia-REPL
Wenn man Julia direkt startet, wird die [Julia-REPL](https://docs.julialang.org/en/v1/stdlib/REPL/) (*read-eval-print* Schleife) gestartet, in der man interaktiv mit Julia arbeiten kann.
```default
$ julia
_
_ _ _(_)_ | Documentation: https://docs.julialang.org
(_) | (_) (_) |
_ _ _| |_ __ _ | Type "?" for help, "]?" for Pkg help.
| | | | | | |/ _` | |
| | |_| | | | (_| | | Version 1.8.5 (2023-01-08)
_/ |\__'_|_|_|\__'_| | Official https://julialang.org/ release
|__/ |
julia>
```
## Arbeiten auf dem [Jupyterhub-Webserver](https://misun103.mathematik.uni-leipzig.de/)
### Jupyterhub & Jupyter
- [Jupyterhub](https://de.wikipedia.org/wiki/Project_Jupyter) ist ein Multi-User-Server für Jupyter.
- Jupyter ist eine web-basierte interaktive Programmierumgebung, die
- ursprünglich in und für Python geschrieben, inzwischen eine
Vielzahl von Programmiersprachen nutzen kann.
- In Jupyter bearbeitet man sogenannte *notebooks*. Das sind strukturiere Textdateien (JSON), erkennbar an der Dateiendung
`*.ipynb`.
Unser Jupyterhub-Server: <https://misun103.mathematik.uni-leipzig.de/>
Nach dem Einloggen in Jupyterhub erscheint ein Dateimanager:
::: {.content-visible when-format="html"}
![](../images/notebook001.png)
:::
::: {.content-visible when-format="pdf"}
![Jupyterhub Dateimanager](../images/notebook001.png){width=50%}
:::
Mit diesem kann man:
- vorhandene *notebooks* öffnen,
- neue *notebooks* anlegen,
- Dateien, z.B. *notebooks*, vom lokalen Rechner hochladen,
- die Sitzung mit `Logout` beenden (bitte nicht vergessen!).
### Jupyter notebooks
::: {.content-visible when-format="html"}
![](../images/notebook003.png)
:::
::: {.content-visible when-format="pdf"}
![Jupyter Notebook](../images/notebook003.png){width=50%}
:::
*Notebooks* bestehen aus Zellen. Zellen können
- Code oder
- Text/Dokumentation (Markdown)
enthalten. In Textzellen kann die Auszeichnungssprache [Markdown](https://de.wikipedia.org/wiki/Markdown) zur Formatierung und LaTeX für mathematische Gleichungen verwendet werden.
::: {.callout-tip }
Bitte die Punkte `User Interface Tour` und `Keyboard Shortcuts` im `Help`-Menü anschauen!
:::
Die Zelle, die man gerade bearbeitet, kann im `command mode` oder `edit mode` sein.
:::{.narrow}
| | *Command mode* | *Edit mode* |
|:-------|:---------------:|:-----------:|
| Mode aktivieren | `ESC` | Doppelklick oder `Enter` in Zelle |
| neue Zelle | `b` | `Alt-Enter` |
| Zelle löschen | `dd` | |
| Notebook speichern | `s` | `Ctrl-s` |
| Notebook umbenennen | `Menu -> File -> Rename` | `Menu -> File -> Rename` |
| Notebook schließen | `Menu -> File -> Close & Halt` | `Menu -> File -> Close & Halt` |
| *run cell* | `Ctrl-Enter` | `Ctrl-Enter` |
| *run cell, move to next cell* | `Shift-Enter` | `Shift-Enter` |
| *run cell, insert new cell below* | `Alt-Enter` | `Alt-Enter` |
: {.striped}
:::
::: {.content-visible when-format="html"}
![](../images/notebook002.png)
:::
::: {.content-visible when-format="pdf"}
![Julia arbeitet...](../images/notebook002.png){width=50%}
:::
Wenn eine Zelle \"arbeitet\", wird ihre Zellen-Nummer zu einem `*` und
die `kernel busy`-Anzeige (Voller schwarzer Punkt oben rechts neben der
Julia-Version) erscheint. Falls das zu lange dauert (ein *infinite loop*
ist schnell programmiert), dann
- `Menu -> Kernel -> Interrupt` anklicken; falls das wirkungslos ist,
- `Menu -> Kernel -> Restart` anklicken.
::: {.callout-important }
Nach einem `kernel restart` müssen alle Zellen, die weiterhin benötigte Definitionen,
`using`-Anweisungen etc enthalten, wieder ausgeführt werden.
**Am Ende der Arbeit bitte immer:**
- `Menu -> File -> Save & Checkpoint`
- `Menu -> File -> Close & Halt`
- `Logout`
:::

318
chapters/first_contact.qmd Normal file
View File

@ -0,0 +1,318 @@
# First Contact
Dieses Kapitel soll beim 'Loslegen' helfen. Es läßt viele Details weg und die Codebeispiele sind oft eher suboptimal.
## Julia als Taschenrechner
```{julia}
#| eval: true
#| echo: false
#| output: false
using REPL, Markdown
function mhelp(s,n,m)
helptxt = Core.eval(Main, REPL.helpmode(s))
helpstr=Markdown.plaininline(helptxt)
print("...\n", join(split(helpstr,'\n')[n:m],'\n'), "\n...")
end;
function Tab(s)
l = filter(x->startswith(x,s), REPL.doc_completions(s))
println.(l[2:end])
return # return nothing, since broadcast println produces empty vector
end
▷ = |>
# IJulia.set_verbose(true)
```
Berechne $\qquad 12^{1/3} + \frac{3\sqrt{2}}{\sin(0.5)-\cos(\frac{\pi}{4})\log(3)}+ e^5$
```{julia}
12^(1/3) + 3sqrt(2) / (sin(.5) - cos(pi/4)*log(3)) + exp(5)
```
Man beachte:
- Potenzen schreibt man `a^b`.
- Die Konstante `pi` ist vordefiniert.
- `log()` ist der natürliche Logarithmus.
- Das Multiplikationszeichen `a*b` kann nach einer Zahl weggelassen werden, wenn eine Variable, Funktion oder Klammer folgt.
## Die wichtigsten Tasten: `Tab` und `?` {#sec-tab}
Man drücke beim Programmieren immer wieder die Tabulatortaste, sobald 2...3 Buchstaben eines Wortes getippt sind. Es werden dann mögliche Ergänzungen angezeigt bzw. ergänzt, wenn die Ergänzung eindeutig ist. Das spart Zeit und bildet ungemein:
```{julia}
lo = "lo" #| hide_line
lo ▷ Tab
```
```{julia}
pri = "pri" #| hide_line
pri ▷ Tab
```
Die eingebaute Julia-Hilfe `?name` zu allen Funktionen und Konstrukten ist sehr umfassend. Hier ein eher kurzes Beispiel:
```{julia}
?for
```
:::{.content-hidden unless-format="xxx"}
::: {.cell }
``` {.julia .cell-code}
?for
```
::: {.cell-output .cell-output-stdout}
```
search: for foreach foldr floor mapfoldr factorial EOFError OverflowError
for loops repeatedly evaluate a block of statements while iterating over a sequence of values.
Examples
julia> for i in [1, 4, 0]
println(i)
end
1
4
0
```
:::
:::
:::
## Variablen und Zuweisungen
Variablen entstehen durch Zuweisung *(assignment)* mit dem Zuweisungsoperator `=` . Danach können sie in weiteren Anweisungen verwendet werden.
```{julia}
x = 1 + sqrt(5)
y = x / 2
```
Im interaktiven Betrieb zeigt Julia das Ergebnis der letzten Operation an.
:::{.callout-note .titlenormal}
Zuweisungen sind keine mathematischen Gleichungen. Die Semantik des Zuweisungsoperators (Gleichheitszeichens) ist:
- berechne die rechte Seite und
- weise das Ergebnis der linken Seite zu.
Ausdrücke wie `x + y = sin(2)` sind daher unzulässig. Links darf nur ein Variablenname stehen.
:::
## Datentypen
Julia ist eine [stark typisierte](https://de.wikipedia.org/wiki/Starke_Typisierung) Sprache. Alle Objekte haben einen Typ.
So gibt es unter anderem die Basistypen
- Ganze Zahlen *(integers)*,
- Gleitkommazahlen *(floating point numbers)*,
- Zeichenketten *(strings)* und
- Wahrheitswerte *(booleans)*.
Den Typ einer Variablen kann man mit der Funktion `typeof()` ermitteln.
```{julia}
#| warning: true
#| error: true
for x ∈ (42, 12.0, 3.3e4, "Hallo!", true)
println("x = ", x, " ..... Typ: ", typeof(x))
end
```
Die Standard-Gleitkommazahl hat eine Länge von 64 Bit, entspricht also einer `double` in C/C++/Java.
Julia ist eine [dynamisch typisierte](https://de.wikipedia.org/wiki/Dynamische_Typisierung) Sprache.
Variablen haben keinen Typ. Sie sind typlose Referenzen (Zeiger) auf Objekte.
Wenn man vom „Typ einer Variablen“ spricht, meint man den Typ des Objektes, das der Variablen gerade zugewiesen ist.
```{julia}
x = sqrt(2)
println( typeof(x), " - Wert von x = $x" )
x = "Jetzt bin ich keine Gleitkommazahl mehr!"
println( typeof(x), " - Wert von x = $x" )
```
## Druckanweisungen
Die Funktion `println()` unterscheidet sich von `print()` dadurch, dass sie am Ende einen Zeilenvorschub ausgibt.
```{julia}
print(y)
print("...die Zeile geht weiter...")
print("immernoch...")
println(y)
println("Neue Zeile")
println("Neue Zeile")
```
Beide Funkionen können als Argument eine Liste von *strings* und Variablen bekommen. Man kann Variablen auch in *strings* einbetten, indem man dem Variablennamen ein Dollarzeichen voranstellt *(string interpolation)*.
```{julia}
x = 23
y = 3x + 5
zz = "Fertig!"
println("x= ", x, " ...und y= ", y, "...", zz) # 1. Variante
println("x= $x ...und y= $y...$zz") # Variante mit string interpolation
```
:::{.callout-note .titlenormal collapse=true icon=false }
## Wenn man ein Dollarzeichen drucken will...
muss man einen *backslash* voranstellen. Wenn man einen *backslash* drucken will, muss man ihn verdoppeln.
```{julia}
println("Ein Dollar: 1\$ und drei backslashs: \\\\\\ ")
```
:::
## Funktionen
Funktionsdefinitionen beginnen mit dem Schlüsselwort `function` und enden mit dem Schlüsselwort `end`. In der Regel haben sie eine oder mehrere Argumente und geben beim Aufruf ein berechnetes Objekt mit der `return`-Anweisung zurück.
```{julia}
function hypotenuse(a, b) # heute besonders umständlich
c2 = a^2 + b^2
c = sqrt(c2)
return c
end
```
Nach ihrer Definition kann die Funktion benutzt (aufgerufen) werden. Die in der Definition verwendeten Variablen `a,b,c,c2` sind lokale Variablen und stehen außerhalb der Funktionsdefinition nicht zur Verfügung.
```{julia}
#| error: true
x = 3
z = hypotenuse(x, 4)
println("z = $z")
println("c = $c")
```
Sehr einfache Funktionen können auch als Einzeiler definiert werden.
```{julia}
hypotenuse(a, b) = sqrt(a^2+b^2)
```
## Tests
Tests liefern einen Wahrheitswert zurück.
```{julia}
x = 3^2
x < 2^3
```
Neben den üblichen arithmetischen Vergleichen `==, !=, <, <= ,> ,>=`
gibt es noch viele andere Tests. Natürlich kann man das Ergebnis eines Tests auch einer Variablen zuweisen, welche dann vom Typ `Bool` ist. Die logischen Operatoren `&&`, `||` und Negation `!` können in Tests verwendet werden.
```{julia}
test1 = "Auto" in ["Fahrrad", "Auto", "Bahn"]
test2 = x == 100 || !(x <= 30 && x > 8)
test3 = startswith("Lampenschirm", "Lamp")
println("$test1 $test2 $test3")
```
## Verzweigungen
Verzweigungen (bedingte Anweisungen) haben die Form
```default
if <Test>
<Anweisung1>
<Anweisung2>
...
end
```
Ein `else`-Zweig und `elseif`-Zweige sind möglich.
```{julia}
x = sqrt(100)
if x > 20
println("Seltsam!")
else
println("OK")
y = x + 3
end
```
Einrückungen verbessern die Lesbarkeit, sind aber fakultativ. Zeilenumbrüche trennen Anweisungen. Das ist auch durch Semikolon möglich. Obiger Codeblock ist für Julia identisch zu folgender Zeile:
```{julia}
# Bitte nicht so programmieren! Sie werden es bereuen!
x=sqrt(100); if x > 20 println("Seltsam!") else println("OK"); y = x + 3 end
```
Es wird dringend empfohlen, von Anfang an den eigenen Code übersichtlich mit sauberen Einrückungen zu formatieren!
## Einfache `for`-Schleifen
zum wiederholten Abarbeiten von Anweisungen haben die Form
```default
for <Zähler> = Start:Ende
<Anweisung1>
<Anweisung2>
...
end
```
Beispiel:
```{julia}
sum = 0
for i = 1:100
sum = sum + i
end
sum
```
## Arrays
1-dimensionale Arrays (Vektoren) sind eine einfache Form von Containern. Man kann sie mit echigen Klammern anlegen
und auf die Elemente per Index zugreifen. Die Indizierung beginnt mit 1.
```{julia}
v = [12, 33.2, 17, 19, 22]
```
```{julia}
typeof(v)
```
```{julia}
v[1] = v[4] + 10
v
```
Man kann leere Vektoren anlegen und sie verlängern.
```{julia}
v = [] # leerer Vektor
push!(v, 42)
push!(v, 13)
v
```
:::{.callout-note icon="false" .titlenormal collapse="true" font-variant-ligatures="no-contextual" }
## Nachtrag: wie die Wirkung der Tab-Taste auf dieser Seite simuliert wurde...
```{julia}
using REPL
function Tab(s)
l = filter(x->startswith(x,s), REPL.doc_completions(s))
println.(l[2:end])
return # return nothing, since broadcast println produces empty vector
end
▷ = |> # https://docs.julialang.org/en/v1/manual/functions/#Function-composition-and-piping
pri = "pri";
```
```{julia}
pri ▷ Tab
```
:::

717
chapters/numerictypes.qmd Normal file
View File

@ -0,0 +1,717 @@
# Maschinenzahlen
```{julia}
for x ∈ ( 3, 3.3e4, Int16(20), Float32(3.3e4), UInt16(9) )
@show x sizeof(x) typeof(x)
println()
end
```
## Ganze Zahlen *(integers)*
Ganze Zahlen werden grundsätzlich als Bitmuster fester Länge gespeichert. Damit ist der Wertebereich endlich.
**Innerhalb dieses Wertebereichs** sind Addition, Subtraktion, Multiplikation und die Ganzzahldivision mit Rest
exakte Operationen ohne Rundungsfehler.
Ganze Zahlen gibt es in den zwei Sorten `Signed` (mit Vorzeichen) und `Unsigned`, die man als Maschinenmodelle für bzw. auffassen kann.
### *Unsigned integers*
```{julia}
subtypes(Unsigned)
```
UInts sind Binärzahlen mit n=8, 16, 32, 64 oder 128 Bits Länge und einem entsprechenden Wertebereich von
$$
0 \le x < 2^n
$$
Sie werden im *scientific computing* eher selten verwendet. Bei hardwarenaher Programmierung dienen sie z.B. dem Umgang mit Binärdaten und Speicheradressen. Deshalb werden sie von Julia standardmäßig auch als Hexadezimalzahlen (mit Präfix `0x` und Ziffern `0-9a-f`) dargestellt.
```{julia}
x = 0x0033efef
@show x typeof(x) Int(x)
z = UInt(32)
@show z typeof(z);
```
### *Signed Integers*
```{julia}
subtypes(Signed)
```
*Integers* haben den Wertebereich
$$
-2^{n-1} \le x < 2^{n-1}
$$
In Julia sind ganze Zahlen standardmäßig 64 Bit groß:
```{julia}
x = 42
typeof(x)
```
Sie haben daher den Wertebereich:
$$
-9.223.372.036.854.775.808 \le x \le 9.223.372.036.854.775.807
$$
Negative Zahlen werden im Zweierkomplement dargestellt:
$x \Rightarrow -x$ entspricht: _flip all bits, then add 1_
Das sieht so aus:
::: {.content-visible when-format="html"}
![Eine Darstellung des fiktiven Datentyps `Int4`](../images/Int4.png){width=50%}
:::
::: {.content-visible when-format="pdf"}
![Eine Darstellung des fiktiven Datentyps `Int4`](../images/Int4.png){width=50%}
:::
Das höchstwertige Bit zeigt das Vorzeichen an. Sein „Umkippen“ entspricht allerdings nicht der Operation `x → -x`.
:::{.callout-important}
Die gesamte Ganzzahlarithmetik ist eine __Arithmetik modulo $2^n$__
```{julia}
x = 2^62 - 10 + 2^62
```
```{julia}
x + 20
```
Keine Fehlermeldung, keine Warnung! Ganzzahlen fester Länge liegen nicht auf einer Geraden, sondern auf einem Kreis!
:::
Schauen wir uns ein paar *Integers* mal an:
```{julia}
using Printf
for i in (1, 2, 7, -7, 2^50, -1, Int16(7), Int8(7))
s = bitstring(i)
t = typeof(i)
@printf "%20i %-5s ⇒ %s\n" i t s
end
```
... und noch etwas mehr *introspection*:
```{julia}
typemin(Int64), typemax(Int64)
```
```{julia}
typemin(UInt64), typemax(UInt64)
```
```{julia}
typemin(Int8), typemax(Int8)
```
## Arithmetik ganzer Zahlen
#### Addition, Multiplikation
Die Operationen `+`,`-`,`*` haben die übliche exakte Arithmetik **modulo $2^{64}$**.
#### Potenzen `a^b`
- Potenzen `a^n` werden für natürliche Exponenten `n` ebenfalls modulo $2^{64}$ exakt berechnet.
- Für negative Exponenten ist das Ergebnis eine Gleitkommazahl.
- `0^0` ist [selbstverständlich](https://en.wikipedia.org/wiki/Zero_to_the_power_of_zero#cite_note-T4n3B-4) gleich 1.
```{julia}
(-2)^63, 2^64, 3^(-3), 0^0
```
- Für natürliche Exponenten wird [*exponentiation by squaring*](https://de.wikipedia.org/wiki/Bin%C3%A4re_Exponentiation) verwendet, so dass z.B. `x^23` nur 7 Multiplikationen benötigt:
$$
x^{23} = \left( \left( (x^2)^2 \cdot x \right)^2 \cdot x \right)^2 \cdot x
$$
#### Division
- Division `/` erzeugt eine Gleitkommazahl.
```{julia}
x = 40/5
```
#### Ganzzahldivision und Rest
- Die Funktionen `div(a,b)`, `rem(a,b)` und `divrem(a,b)` berechnen den Quotient der Ganzzahldivision, den dazugehörigen Rest *(remainder)* bzw. beides als Tupel.
- Für `div(a,b)` gibt es die Operatorform `a ÷ b` (Eingabe: `\div<TAB>`) und für `rem(a,b)` die Operatorform `a % b`.
- Standardmäßig wird bei der Division „zur Null hin gerundet“, wodurch der dazugehörige Rest dasselbe Vorzeichen wie der Dividend `a` trägt:
```{julia}
@show divrem( 27, 4)
@show ( 27 ÷ 4, 27 % 4)
@show (-27 ÷ 4, -27 % 4 )
@show ( 27 ÷ -4, 27 % -4);
```
- Eine von `RoundToZero` abweichende Rundungsregel kann bei den Funktionen als optionales 3. Argument angegeben werden.
- `?RoundingMode` zeigt die möglichen Rundungsregeln an.
- Für die Rundungsregel `RoundDown` („in Richtung minus unendlich"), wodurch der dazugehörige Rest dasselbe Vorzeichen wie der Divisor `b` bekommt, gibt es auch die Funktionen `fld(a,b)` *(floored division)* und `mod(a,b)`:
```{julia}
@show divrem(-27, 4, RoundDown)
@show (fld(-27, 4), mod(-27, 4))
@show (fld( 27, -4), mod( 27, -4));
```
Für alle Rundungsregeln gilt:
```
div(a, b, RoundingMode) * b + rem(a, b, RoundingMode) = a
```
#### Der Datentyp `BigInt`
Der Datentyp `BigInt` ermöglicht Ganzzahlen beliebiger Länge. Der benötigte Speicher wird dynamisch allokiert.
Numerische Konstanten haben automatisch einen ausreichend großen Typ:
```{julia}
z = 10
@show typeof(z)
z = 10_000_000_000_000_000 # 10 Billiarden
@show typeof(z)
z = 10_000_000_000_000_000_000 # 10 Trillionen
@show typeof(z)
z = 10_000_000_000_000_000_000_000_000_000_000_000_000_000 # 10 Sextilliarden
@show typeof(z);
```
Meist wird man allerdings den Datentyp `BigInt` explizit anfordern müssen, damit nicht modulo $2^{64}$ gerechnet wird:
```{julia}
@show 3^300 BigInt(3)^300;
```
*Arbitrary precision arithmetic* kostet einiges an Speicherplatz und Rechenzeit.
Wir vergleichen den Zeit- und Speicherbedarf bei der Aufsummation von 10 Millionen Ganzzahlen als `Int64` und als `BigInt`.
```{julia}
# 10^7 Zufallszahlen, gleichverteilt zwischen -10^7 und 10^7
vec_int = rand(-10^7:10^7, 10^7)
# Dieselben Zahlen als BigInts
vec_bigint = BigInt.(vec_int)
```
Einen ersten Eindruck vom Zeit- und Speicherbedarf gibt das `@time`-Macro:
```{julia}
@time x = sum(vec_int)
@show x typeof(x)
```
```{julia}
@time x = sum(vec_bigint)
@show x typeof(x);
```
Durch die Just-in-Time-Compilation von Julia ist die einmalige Ausführung einer Funktion wenig aussagekräftig. Das Paket `BenchmarkTools` stellt u.a. das Macro `@benchmark` bereit, das eine Funktion mehrfach aufruft und die Ausführungszeiten als Histogramm darstellt.
:::{.ansitight}
```{julia}
using BenchmarkTools
@benchmark sum($vec_int)
```
```{julia}
@benchmark sum($vec_bigint)
```
:::
Die `BigInt`-Addition ist mehr als 30 mal langsamer.
:::{.content-hidden unless-format="xxx"}
Die folgende Funktion soll die Summe aller Zahlen von 1 bis n mit der Arithmetik des Datentyps T berechnen.
Auf Grund der *type promotion rules* reicht es für `T ≥ Int64` dazu aus, die Akkumulatorvariable mit einer Zahl vom Typ T zu initialisieren.
```{julia}
function mysum(n, T)
s = T(0)
for i = 1:n
s += i
end
return s
end
```
Einen ersten Eindruck vom Zeit- und Speicherbedarf gibt das `@time`-Macro:
```{julia}
@time x = mysum(10_000_000, Int64)
@show x typeof(x);
```
```{julia}
@time x = mysum(10_000_000, BigInt)
@show x typeof(x);
```
Durch die Just-in-Time-Compilation von Julia ist die einmalige Ausführung einer Funktion wenig aussagekräftig. Das Paket `BenchmarkTools` stellt u.a. das Macro `@benchmark` bereit, das eine Funktion mehrfach aufruft und die Ausführungszeiten als Histogramm darstellt.
:::{.ansitight}
```{julia}
using BenchmarkTools
@benchmark mysum(10_000_000, Int64)
```
```{julia .myclass}
@benchmark mysum(10_000_000, BigInt)
```
Die Berechnung von $\sum_{n=1}^{10000000} n$ dauert auf meinem PC mit den Standard-64bit-Integern im Schnitt 2 Nanosekunden, in *arbitrary precision arithmetic* über eine Sekunde, wobei auch noch fast 500MB Speicher allokiert werden.
:::
:::
## Gleitkommazahlen
Aus _floating point numbers_ kann man im Deutschen **[Gleit|Fließ]--[Komma|Punkt]--Zahlen** machen
und tatsächlich kommen alle 4 Varianten in der Literatur vor.
In der Numerischen Mathematik spricht man auch gerne von **Maschinenzahlen**.
### Grundidee
- Eine „feste Anzahl von Vor- und Nachkommastellen“ ist für viele Probleme ungeeignet.
- Eine Trennung zwischen „gültigen Ziffern“ und Größenordnung wie in der wissenschaftlichen Notation
$$ 345.2467 \times 10^3\qquad 34.52467\times 10^4\qquad \underline{3.452467\times 10^5}\qquad 0.3452467\times 10^6\qquad 0.03452467\times 10^7$$
ist wesentlich flexibler.
- Zur Eindeutigkeit legt man fest: Die Darstellung mit genau einer Ziffer vor dem Komma ist die __Normalisierte Darstellung__.
- Bei Binärzahlen `1.01101`: ist diese Ziffer immer gleich Eins und man kann auf das Abspeichern dieser Ziffer verzichten.
:::{.callout-note }
## Maschinenzahlen
Die Menge der Maschinenzahlen $𝕄(b, m, e_{min}, e_{max})$ ist charakterisiert durch die verwendete Basis $b$, die Mantissenlänge $m$ und den Wertebereich des Exponenten $\{e_{min}, ... ,e_{max}\}$.
In unserer Konvention hat die Mantisse einer normalisierten Maschinenzahl eine Ziffer (der Basis $b$) ungleich Null vor dem Komma und $m-1$ Nachkommastellen.
Wenn $b=2$ ist, braucht man daher nur $m-1$ Bits zur Speicherung der Mantisse normalisierter Gleitkommazahlen.
Der Standard IEEE 754, der von der Mahrzahl der modernen Prozessoren und Programmiersprachen implementiert wird, definiert
- `Float32` als $𝕄(2, 24, -126, 127 )$ und
- `Float64` als $𝕄(2, 53, -1022, 1023 ).$
:::
### Aufbau von `Float64` nach [Standard IEEE 754](https://de.wikipedia.org/wiki/IEEE_754)
::: {.content-visible when-format="html"}
![Aufbau einer `Float64` (Quelle:^[Quelle: <a href="https://commons.wikimedia.org/wiki/File:IEEE_754_Double_Floating_Point_Format.svg">Codekaizen</a>, <a href="https://creativecommons.org/licenses/by-sa/4.0">CC BY-SA 4.0</a>, via Wikimedia Commons ])
](../images/1024px-IEEE_754_Double_Floating_Point_Format.png)
:::
::: {.content-visible when-format="pdf"}
![Aufbau einer `Float64` \mysmall{(Quelle: \href{https://commons.wikimedia.org/wiki/File:IEEE_754_Double_Floating_Point_Format.svg}{Codekaizen}, \href{https://creativecommons.org/licenses/by-sa/4.0}{CC BY-SA 4.0}, via Wikimedia Commons)}
](../images/1024px-IEEE_754_Double_Floating_Point_Format.png){width="70%"}
:::
- 1 Vorzeichenbit $S$
- 11 Bits für den Exponenten, also $0\le E \le 2047$
- die Werte $E=0$ und $E=(11111111111)_2=2047$ sind reserviert für die Codierung von Spezialwerten wie
$\pm0, \pm\infty$, NaN _(Not a Number)_ und denormalisierte Zahlen.
- 52 Bits für die Mantisse $M,\quad 0\le M<1$, das entspricht etwa 16 Dezimalstellen
- Damit wird folgende Zahl dargestellt:
$$ x=(-1)^S \cdot(1+M)\cdot 2^{E-1023}$$
Ein Beispiel:
```{julia}
x = 27.56640625
bitstring(x)
```
Das geht auch schöner:
```{julia}
function printbitsf64(x::Float64)
s = bitstring(x)
printstyled(s[1], color = :blue, reverse=true)
printstyled(s[2:12], color = :green, reverse=true)
printstyled(s[13:end], color=:red, bold=true, reverse=true)
print("\n")
end
printbitsf64(x)
```
und wir können S (in blau),E (grün) und M (rot) ablesen:
$$
\begin{aligned}
S &= 0\\
E &= (10000000011)_2 = 2^{10} + 2^1 + 2^0 = 1027\\
M &= (.1011100100001)_2 = \frac{1}{2} + \frac{1}{2^3} + \frac{1}{2^4} + \frac{1}{2^5} + \frac{1}{2^8} + \frac{1}{2^{12}}\\
x &=(-1)^S \cdot(1+M)\cdot 2^{E-1023}
\end{aligned}
$$
... und damit die Zahl rekonstruieren:
```{julia}
x = (1 + 1/2 + 1/8 + 1/16 + 1/32 + 1/256 + 1/4096) * 2^4
```
- Die Maschinenzahlen 𝕄 bilden eine endliche, diskrete Untermenge von . Es gibt eine kleinste und eine größte Maschinenzahl und abgesehen davon haben alle x∈𝕄 einen Vorgänger und Nachfolger in 𝕄.
- Was ist der Nachfolger von x in 𝕄? Dazu setzen wir das kleinste Mantissenbit von 0 auf 1.
- Die Umwandlung eines Strings aus Nullen und Einsen in die entsprechende Maschinenzahl ist z.B. so möglich:
```{julia}
ux = parse(UInt64, "0100000000111011100100010000000000000000000000000000000000000001", base=2)
reinterpret(Float64, ux)
```
Das kann man in Julia allerdings auch einfacher haben:
```{julia}
y = nextfloat(x)
```
Der Vorgänger von x ist:
```{julia}
z = prevfloat(x)
println(z)
printbitsf64(z)
```
## Maschinenepsilon
- Den Abstand zwischen `1` und dem Nachfolger `nextfloat(1)` nennt man [**Maschinenepsilon**](https://en.wikipedia.org/wiki/Machine_epsilon).
- Für `Float64` mit einer Mantissenlänge von 52 Bit, ist $\epsilon=2^{-52}$.
```{julia}
@show nextfloat(1.) - 1 2^-52 eps(Float64);
```
- Das Maschinenepsilon ist ein Maß für den relativen Abstand zwischen den Maschinenzahlen und quantifiziert die Aussage: „64-Bit-Gleitkommazahlen haben eine Genauigkeit von etwa 16 Dezimalstellen.“
- Das Maschinenepsilon ist etwas völlig anderes als die kleinste von Null verschiedene Gleitkommazahl:
```{julia}
floatmin(Float64)
```
- Ein Teil der Literatur verwendet eine andere Definition des Maschinenepsilons, die halb so groß ist.
$$
\epsilon' = \frac{\epsilon}{2}\approx 1.1\times 10^{-16}
$$
ist der maximale relative Fehler, der beim Runden einer reellen Zahl auf die nächste Maschinenzahl entstehen kann.
- Da Zahlen aus dem Intervall $(1-\epsilon',1+\epsilon']$ auf die Maschinenzahl $1$ gerundet werden, kann man $\epsilon'$ auch definieren als: *die größte Zahl, für die in der Maschinenzahlarithmetik noch gilt: $1+\epsilon' = 1$.*
Auf diese Weise kann man das Maschinenepsilon auch berechnen:
:::{.ansitight}
```{julia}
Eps = 1
while(1 != 1 + Eps)
Eps /= 2
println(1+Eps)
end
Eps
```
oder als Bitmuster:
```{julia}
Eps=1
while 1 != 1 + Eps
Eps/= 2
printbitsf64(1+Eps)
end
Eps
```
:::
Die größte und die kleinste positive normalisiert darstellbare Gleitkommazahl eines Gleitkommatyps kann man abfragen:
```{julia}
@show floatmax(Float64)
printbitsf64(floatmax(Float64))
@show floatmin(Float64)
printbitsf64(floatmin(Float64))
```
## Runden auf Maschinenzahlen
- Die Abbildung rd: $\rightarrow$ 𝕄 soll zur nächstgelegenen darstellbaren Zahl runden.
- Standardrundungsregel: _round to nearest, ties to even_
Wenn man genau die Mitte zwischen zwei Maschinenzahlen trifft *(tie)*, wählt man die, deren letztes Mantissenbit 0 ist.
- Begründung: damit wird statistisch in 50% der Fälle auf- und in 50% der Fälle abgerundet und so ein „statistischer Drift“ bei längeren Rechnungen vermieden.
## Maschinenzahlarithmetik
Die Maschinenzahlen sind als Untermenge von nicht algebraisch abgeschlossen. Schon die Summe zweier Maschinenzahlen wird in der Regel keine Maschinenzahl sein.
:::{.callout-important}
Der Standard IEEE 754 fordert, dass die Maschinenzahlarithmetik das *gerundete exakte Ergebnis* liefert:
Das Resultat muss gleich demjenigen sein, das bei einer exakten Ausführung der entsprechenden Operation mit anschließender Rundung entsteht.
$$
a \oplus b = \text{rd}(a + b)
$$
Analoges muss für die Implemetierung der Standardfunktionen wie
wie `sqrt()`, `log()`, `sin()` ... gelten: Sie liefern ebenfalls die Maschinenzahl, die dem exakten Ergebnis am nächsten kommt.
:::
Die Arithmetik ist *nicht assoziativ*:
```{julia}
1 + 10^-16 + 10^-16
```
```{julia}
1 + (10^-16 + 10^-16)
```
Im ersten Fall (ohne Klammern) wird von links nach rechts ausgewertet:
$$
\begin{aligned}
1 \oplus 10^{-16} \oplus 10^{-16} &=
(1 \oplus 10^{-16}) \oplus 10^{-16} \\ &=
\text{rd}\left(\text{rd}(1 + 10^{-16}) + 10^{-16} \right)\\
&= \text{rd}(1 + 10^{-16})\\
&= 1
\end{aligned}
$$
Im zweiten Fall erhält man:
$$
\begin{aligned}
1 \oplus \left(10^{-16} \oplus 10^{-16}\right) &=
\text{rd}\left(1 + \text{rd}(10^{-16} + 10^{-16}) \right)\\
&= \text{rd}(1 + 2*10^{-16})\\
&= 1.0000000000000002
\end{aligned}
$$
Es sei auch daran erinnert, dass sich selbst „einfache“ Dezimalbrüche häufig nicht exakt als Maschinenzahlen darstellen lassen:
$$
\begin{aligned}
(0.1)_{10} &= (0.000110011001100110011001100...)_2 = (0.000\overline{1100})_2 \\
(0.3)_{10} &= (0.0100110011001100110011001100..)_2 = (0.0\overline{1001})_2
\end{aligned}
$$
```{julia}
printbitsf64(0.1)
printbitsf64(0.3)
```
Folge:
```{julia}
0.1 + 0.1 == 0.2
```
```{julia}
0.2 + 0.1 == 0.3
```
```{julia}
0.2 + 0.1
```
Bei der Ausgabe einer Maschinenzahl muss der Binärbruch in einen Dezimalbruch entwickelt werden. Man kann sich auch mehr Stellen dieser Dezimalbruchentwicklung anzeigen lassen:
```{julia}
using Printf
@printf("%.30f", 0.1)
```
```{julia}
@printf("%.30f", 0.3)
```
Die Binärbruch-Mantisse einer Maschinenzahl kann eine lange oder sogar unendlich-periodische Dezimalbruchentwicklung haben. Dadurch
sollte man sich nicht eine „höheren Genauigkeit“ suggerieren lassen!
:::{.callout-important}
Moral: wenn man `Float`s auf Gleichheit testen will, sollte man fast immer eine dem Problem angemessene realistische Genauigkeit `epsilon` festlegen und
darauf testen:
```julia
epsilon = 1.e-10
if abs(x-y) < epsilon
# ...
end
```
:::
## Normalisierte und Denormalisierte Maschinenzahlen
Zum Verständnis nehmen wir ein einfaches Modell:
- Sei 𝕄(10,4,±5) die Menge der Maschinenzahlen zur Basis 10 mit 4 Mantissenstellen (eine vor dem Komma, 3 Nachkommastellen) und dem Exponentenbereich -5 ≤ E ≤ 5.
- Dann ist die normalisierte Darstellung (Vorkommastelle ungleich 0)
von z.B. 1234.0 gleich 1.234e3 und von 0.00789 gleich 7.890e-3.
- Es ist wichtig, dass die Maschinenzahlen bei jedem Rechenschritt normalisiert gehalten werden. Nur so wird die Mantissenlänge voll ausgenutzt und die Genauigkeit ist maximal.
- Die kleinste positive normalisierte Zahl in unserem Modell ist `x = 1.000e-5`. Schon `x/2` müsste auf 0 gerundet werden.
- Hier erweist es sich für viele Anwendungen als günstiger, auch denormalisierte *(subnormal)* Zahlen zuzulassen und `x/2` als `0.500e-5` oder `x/20` als `0.050e-5` darzustellen.
- Dieser *gradual underflow* ist natürlich mit einem Verlust an gültigen Stellen und damit Genauigkeit verbunden.
Im `Float`-Datentyp werden solche *subnormal values* dargestellt durch ein Exponentenfeld, in dem alle Bits gleich Null sind:
```{julia}
#| echo: false
#| output: false
flush(stdout)
```
:::{.ansitight}
```{julia}
using Printf
x = 2 * floatmin(Float64) # 2*kleinste normalisierte Gleitkommazahl > 0
while x != 0
x /= 2
@printf "%14.6g " x
printbitsf64(x)
end
```
:::
## Spezielle Werte
Die Gleitkommaarithmetik kennt einige spezielle Werte:
```{julia}
for x ∈ (NaN, Inf, -Inf, -0.0)
@printf "%6g " x
printbitsf64(x)
end
```
- Ein Exponentenüberlauf *(overflow)* führt zum Ergebnis `Inf` oder `-Inf`.
```{julia}
2/0, -3/0, floatmax(Float64) * 1.01, exp(1300)
```
- Damit kann weitergerechnet werden:
```{julia}
-Inf + 20, Inf/30, 23/-Inf, sqrt(Inf), Inf * 0, Inf - Inf
```
- `NaN` *(Not a Number)* steht für das Resultat einer Operation, das undefiniert ist. Alle weiteren Operationen mit `NaN` ergeben ebenfalls `NaN`.
```{julia}
0/0, Inf - Inf, 2.3NaN, sqrt(NaN)
```
- Da `NaN` einen undefinierten Wert repräsentiert, ist es zu nichts gleich, nichtmal zu sich selbst. Das ist sinnvoll, denn wenn zwei Variablen `x` und `y` als `NaN` berechnet wurden, sollte man nicht schlußfolgern, dass sie gleich sind.
- Zum Testen auf `NaN` gibt es daher die boolsche Funktion `isnan()`.
```{julia}
x = 0/0
y = Inf - Inf
@show x==y NaN==NaN isfinite(NaN) isinf(NaN) isnan(x) isnan(y);
```
- Es gibt eine „minus Null“. Sie signalisiert einen Exponentenunterlauf *(underflow)* einer betragsmäßig zu klein gewordenen *negativen* Größe.
```{julia}
@show 23/-Inf -2/exp(1200) -0.0==0.0;
```
## Mathematische Funktionen
Julia verfügt über die [üblichen mathematischen Funktionen](https://docs.julialang.org/en/v1/manual/mathematical-operations/#Rounding-functions)
`sqrt, exp, log, log2, log10, sin, cos,..., asin, acos,..., sinh,..., gcd, lcm, factorial,...,abs, max, min,...`,
darunter z.B. die [Rundungsfunktionen](https://de.wikipedia.org/wiki/Abrundungsfunktion_und_Aufrundungsfunktion)
- `floor(T,x)` = $\lfloor x \rfloor$
- `ceil(T,x)` = $\lceil x \rceil$
```{julia}
floor(3.4), floor(Int64, 3.5), floor(Int64, -3.5)
```
```{julia}
ceil(3.4), ceil(Int64, 3.5), ceil(Int64, -3.5)
```
Es sei noch hingewiesen auf `atan(y, x)`, den [Arkustangens mit 2 Argumenten](https://de.wikipedia.org/wiki/Arctan2), Er ist in anderen Programmiersprachen oft als Funktion mit eigenem Namen *atan2* implementiert.
Dieser löst das Problem der Umrechnung von kartesischen in Polarkoordinaten ohne lästige Fallunterscheidung.
- `atan(y,x)` ist Winkel der Polarkoordinaten von (x,y) im Intervall $(-\pi,\pi]$. Im 1. und 4. Quadranten ist er also gleich `atan(y/x)`
```{julia}
atan(3, -2), atan(-3, 2), atan(-3/2)
```
## Umwandlung Strings $\Longleftrightarrow$ Zahlen
Die Umwandlung ist mit den Funktionen `parse()` und `string()` möglich.
```{julia}
parse(Int64, "1101", base=2)
```
```{julia}
string(13, base=2)
```
```{julia}
string(1/7)
```
```{julia}
string(77, base=16)
```
Zur Umwandlung der numerischen Typen ineinander kann man die Typnamen verwenden. Typenamen sind auch Konstruktoren:
```{julia}
x = Int8(67)
@show x typeof(x);
```
```{julia}
z = UInt64(3459)
```
```{julia}
y = Float64(z)
```
## Literatur
- D. Goldberg, [What Every Computer Scientist Should Know About Floating-Point Arithmetic](http://www.validlab.com/goldberg/paper.pdf)
- C. Vuik, [Some Disasters caused by numerical errors](http://ta.twi.tudelft.nl/users/vuik/wi211/disasters.html)

459
chapters/pcomplex.qmd Normal file
View File

@ -0,0 +1,459 @@
# Ein Fallbeispiel: Der parametrisierte Datentyp PComplex
Wir wollen als neuen numerischen Typen **komplexe Zahlen in Polardarstellung $z=r e^{i\phi}=(r,ϕ)$** einführen.
- Der Typ soll sich in die Typhierarchie einfügen als Subtyp von 'Number'.
- $r$ und $\phi$ sollen Gleitkommazahlen sein. (Im Unterschied zu komplexen Zahlen in 'kartesischen' Koordinaten hat eine Einschränkung auf ganzzahlige Werte von r oder ϕ mathematisch wenig Sinn.)
## Die Definition von `PComplex`
Ein erster Versuch könnte so aussehen:
```{julia}
struct PComplex1{T <: AbstractFloat} <: Number
r :: T
ϕ :: T
end
z1 = PComplex1(-32.0, 33.0)
z2 = PComplex1{Float32}(12, 13)
@show z1 z2;
```
Julia stellt automatisch *default constructors* zur Verfügung:
- den Konstruktor `PComplex1`, bei dem der Typ `T` von den übergebenen Argumenten abgeleitet wird und
- Konstruktoren `PComplex{Float64},...` mit expliziter Typangabe. Hier wird versucht, die Argumente in den angeforderten Typ zu konvertieren.
------
Wir wollen nun, dass der Konstruktor noch mehr tut.
In der Polardarstellung soll $0\le r$ und $0\le \phi<2\pi$ gelten.
Wenn die übergebenen Argumente das nicht erfüllen, sollten sie entsprechend umgerechnet werden.
Dazu definieren wir einen _inner constructor_, der den _default constructor_ ersetzt.
- Ein _inner constructor_ ist eine Funktion innerhalb der `struct`-Definition.
- In einem _inner constructor_ kann man die spezielle Funktion `new` verwenden, die wie der _default constructor_ wirkt.
```{julia}
struct PComplex{T <: AbstractFloat} <: Number
r :: T
ϕ :: T
function PComplex{T}(r::T, ϕ::T) where T<:AbstractFloat
if r<0 # flip the sign of r and correct phi
r = -r
ϕ += π
end
if r==0 ϕ=0 end # normalize r=0 case to phi=0
ϕ = mod(ϕ, 2π) # map phi into interval [0,2pi)
new(r, ϕ) # new() ist special function,
end # available only inside inner constructors
end
```
```{julia}
#| echo: false
#| output: false
#=
in den ganzen quarto-runs wollen bir hier noch das default-show benutzen
=#
zz = @which Base.show(stdout, PComplex{Float64}(2.,3.))
if zz.module != Base
Base.delete_method(zz)
end
```
```{julia}
z1 = PComplex{Float64}(-3.3, 7π+1)
```
Für die explizite Angabe eines *inner constructors* müssen wir allerdings einen Preis zahlen: Die sonst von Julia bereitgestellten *default constructors* fehlen.
Den Konstruktor, der ohne explizite Typangabe in geschweiften Klammern auskommt und den Typ der Argumente übernimmt, wollen wir gerne auch haben:
```{julia}
PComplex(r::T, ϕ::T) where {T<:AbstractFloat} = PComplex{T}(r,ϕ)
z2 = PComplex(2.0, 0.3)
```
## Eine neue Schreibweise
Julia verwendet `//` als Infix-Konstruktor für den Typ `Rational`. Sowas Schickes wollen wir auch.
In der Elektronik/Elektrotechnik werden [Wechselstromgrößen durch komplexe Zahlen beschrieben.](https://de.wikipedia.org/wiki/Komplexe_Wechselstromrechnung). Dabei ist eine Darstellung komplexer Zahlen durch "Betrag" und "Phase" üblich und sie wird gerne in der sogenannten [Versor-Form](https://de.wikipedia.org/wiki/Versor) (engl. *phasor*) dargestellt:
$$
z= r\enclose{phasorangle}{\phi} = 3.4\;\enclose{phasorangle}{45^\circ}
$$
wobei man in der Regel den Winkel in Grad notiert.
:::{.callout-note .titlenormal collapse="true"}
## Mögliche Infix-Operatoren in Julia
In Julia ist eine große Anzahl von Unicode-Zeichen reserviert für die Verwendung als Operatoren. Die definitive Liste ist im [Quellcode des Parsers.](https://github.com/JuliaLang/julia/blob/eaa2c58aeb12f27c1d8c116ab111773a4fc4495f/src/julia-parser.scm#L13-L31)
Auf Details werden wir in einem späteren Kapitel noch eingehen.
Und ja, der Julia-Parser ist in einem Lisp(genauer: Scheme)-Dialekt geschrieben. In Julia ist ein kleiner Scheme-Interpreter namens [femtolisp](https://github.com/JeffBezanson/femtolisp) integriert. Geschrieben hat ihn einer der "Väter" von Julia bevor er mit der Arbeit an Julia begann.
:::
Das Winkel-Zeichen `∠` steht leider nicht als Operatorsymbol zur Verfügung. Wir weichen aus auf `⋖`. Das kann in Julia als als `\lessdot<tab>` eingegeben werden.
```{julia}
⋖(r::Real, ϕ::Real) = PComplex(r, π*ϕ/180)
z3 = 2. ⋖ 90.
```
(Die Typ-Annotation -- `Real` statt `AbstractFloat` -- ist ein Vorgriff auf kommende weitere Konstruktoren. Im Moment funktioniert der Operator `⋖` erstmal nur mit `Float`s.)
Natürlich wollen wir auch die Ausgabe so schön haben. Details dazu findet man in der [Dokumentation](https://docs.julialang.org/en/v1/manual/types/#man-custom-pretty-printing).
```{julia}
using Printf
function Base.show(io::IO, z::PComplex)
# wir drucken die Phase in Grad, auf Zehntelgrad gerundet,
p = z.ϕ * 180/π
sp = @sprintf "%.1f" p
print(io, z.r, "⋖", sp, '°')
end
@show z3;
```
## Methoden für `PComplex`
Damit unser Typ ein anständiges Mitglied der von `Number` abstammenden Typfamilie wird, brauchen wir allerdings noch eine ganze Menge mehr. Es müssen Arithmetik, Vergleichsoperatoren, Konvertierungen usw. definiert werden.
Wir beschränken uns auf Multiplikation und Quadratwurzeln.
:::{.callout-note collapse="true"}
## Module
- Um die `methods` der existierenden Funktionen und Operationen zu ergänzen, muss man diese mit ihrem 'vollen Namen' ansprechen.
- Alle Objekte gehören zu einem Namensraum oder `module`.
- Die meisten Basisfunktionen gehören zum Modul `Base`, welches standardmäßig immer ohne explizites `using ...` geladen wird.
- Solange man keine eigenen Module definiert, sind die eigenen Definitionen im Modul `Main`.
- Das Macro `@which`, angewendet auf einen Namen, zeigt an, in welchem Modul der Name definiert wurde.
```{julia}
f(x) = 3x^3
@which f
```
```{julia}
wp = @which +
ws = @which(sqrt)
println("Modul für Addition: $wp, Modul für sqrt: $ws")
```
:::
```{julia}
qwurzel(z::PComplex) = PComplex(sqrt(z.r), z.ϕ / 2)
```
```{julia}
#| echo: false
#| output: false
#=
damit das length(methods(sqrt)) klappt
=#
if hasmethod(sqrt, (PComplex,))
zz = @which Base.sqrt(PComplex{Float64}(1.,1.))
Base.delete_method(zz)
end
```
Die Funktion `sqrt()` hat schon einige Methoden:
```{julia}
length(methods(sqrt))
```
Jetzt wird es eine Methode mehr:
```{julia}
Base.sqrt(z::PComplex) = qwurzel(z)
length(methods(sqrt))
```
```{julia}
sqrt(z2)
```
und nun zur Multiplikation:
```{julia}
Base.:*(x::PComplex, y::PComplex) = PComplex(x.r * y.r, x.ϕ + y.ϕ)
@show z1 * z2;
```
(Da das Operatorsymbol kein normaler Name ist, muss der Doppelpunkt bei der Zusammensetzung mit `Base.` sein.)
Wir können allerdings noch nicht mit anderen numerischen Typen multiplizieren. Dazu könnte man nun eine Vielzahl von entsprechenden Methoden definieren. Julia stellt *für numerische Typen* noch einen weiteren Mechanismus zur Verfügung, der das etwas vereinfacht.
## Typ-Promotion und Konversion
In Julia kann man bekanntlich die verschiedensten numerischen Typen nebeneinander verwenden.
```{julia}
1//3 + 5 + 5.2 + 0xff
```
Wenn man in die zahlreichen Methoden schaut, die z.B. für `+` und `*` definiert sind, findet man u.a. eine Art 'catch-all-Definition'
```julia
+(x::Number, y::Number) = +(promote(x,y)...)
*(x::Number, y::Number) = *(promote(x,y)...)
```
(Die 3 Punkte sind der splat-Operator, der das von promote() zurückgegebene Tupel wieder in seine Bestandteile zerlegt.)
Da die Methode mit den Typen `(Number, Number)` sehr allgemein ist, wird sie erst verwendet, wenn spezifischere Methoden nicht greifen.
Was passiert hier?
### Die Funktion `promote(x,y,...)`
Diese Funktion versucht, alle Argumente in einen gemeinsamen Typen umzuwandeln, der alle Werte (möglichst) exakt darstellen kann.
```{julia}
promote(12, 34.555, 77/99, 0xff)
```
```{julia}
z = promote(BigInt(33), 27)
@show z typeof(z);
```
Die Funktion `promote()` verwendet dazu zwei Helfer, die Funktionen
`promote_type(T1, T2)` und `convert(T, x)`
Wie üblich in Julia, kann man diesen Mechanismus durch [eigene *promotion rules* und `convert(T,x)`-Methoden erweitern.](https://docs.julialang.org/en/v1/manual/conversion-and-promotion/)
### Die Funktion `promote_type(T1, T2,...)`
Sie ermittelt, zu welchem Typ umgewandelt werden soll. Argumente sind Typen, nicht Werte.
```{julia}
@show promote_type(Rational{Int64}, ComplexF64, Float32);
```
### Die Funktion `convert(T,x)`
Die Methoden von
`convert(T, x)` wandeln `x` in ein Objekt vom Typ `T` um. Dabei sollte eine solche Umwandlung verlustfrei möglich sein.
```{julia}
z = convert(Float64, 3)
```
```{julia}
z = convert(Int64, 23.00)
```
```{julia}
z = convert(Int64, 2.3)
```
Die spezielle Rolle von `convert()` liegt darin, dass es an verschiedenen Stellen _implizit_ und automatisch eingesetzt wird:
> [The following language constructs call convert](https://docs.julialang.org/en/v1/manual/conversion-and-promotion/#When-is-convert-called?):
>
- Assigning to an array converts to the array's element type.
- Assigning to a field of an object converts to the declared type of the field.
- Constructing an object with new converts to the object's declared field types.
- Assigning to a variable with a declared type (e.g. local x::T) converts to that type.
- A function with a declared return type converts its return value to that type.
-- und natürlich in `promote()`
Für selbstdefinierte Datentypen kann man convert() um weitere Methoden ergänzen.
Für Datentypen innerhalb der Number-Hierarchie gibt es wieder eine 'catch-all-Definition'
```julia
convert(::Type{T}, x::Number) where {T<:Number} = T(x)
```
Also: Wenn für einen Typen `T` aus der Hierarchie `T<:Number` ein Konstruktor `T(x)` mit einem numerischen Argument `x` existiert, dann wird dieser Konstruktor `T(x)` automatisch für Konvertierungen benutzt. (Natürlich können auch speziellere Methoden für `convert()` definiert werden, die dann Vorrang haben.)
### Weitere Konstruktoren für `PComplex`
```{julia}
## (a) r, ϕ beliebige Reals, z.B. Integers, Rationals
PComplex{T}(r::T1, ϕ::T2) where {T<:AbstractFloat, T1<:Real, T2<: Real} =
PComplex{T}(convert(T, r), convert(T, ϕ))
PComplex(r::T1, ϕ::T2) where {T1<:Real, T2<: Real} =
PComplex{promote_type(Float64, T1, T2)}(r, ϕ)
## (b) Zur Umwandlung von Reals: Konstruktor mit
## nur einem Argument r
PComplex{T}(r::S) where {T<:AbstractFloat, S<:Real} =
PComplex{T}(convert(T, r), convert(T, 0))
PComplex(r::S) where {S<:Real} =
PComplex{promote_type(Float64, S)}(r, 0.0)
## (c) Umwandlung Complex -> PComplex
PComplex{T}(z::Complex{S}) where {T<:AbstractFloat, S<:Real} =
PComplex{T}(abs(z), angle(z))
PComplex(z::Complex{S}) where {S<:Real} =
PComplex{promote_type(Float64, S)}(abs(z), angle(z))
```
Ein Test der neuen Konstruktoren:
```{julia}
3//5 ⋖ 45, PComplex(Complex(1,1)), PComplex(-13)
```
Wir brauchen nun noch *promotion rules*, die festlegen, welcher Typ bei `promote(x::T1, y::T2)` herauskommen soll. Damit wird `promote_type()` intern um die nötigen weiteren Methoden erweitert.
### *Promotion rules* für `PComplex`
```{julia}
Base.promote_rule(::Type{PComplex{T}}, ::Type{S}) where {T<:AbstractFloat,S<:Real} =
PComplex{promote_type(T,S)}
Base.promote_rule(::Type{PComplex{T}}, ::Type{Complex{S}}) where
{T<:AbstractFloat,S<:Real} = PComplex{promote_type(T,S)}
```
1. Regel:
: Wenn ein `PComplex{T}` und ein `S<:Real` zusammentreffen, dann sollen beide zu `PComplex{U}` umgewandelt werden, wobei `U` der Typ ist, zu dem `S` und `T` beide umgewandelt (_promoted_) werden können.
2. Regel
: Wenn ein `PComplex{T}` und ein `Complex{S}` zusammentreffen, dann sollen beide zu `PComplex{U}` umgewandelt werden, wobei `U` der Typ ist, zu dem `S` und `T` beide umgewandelt werden können.
Damit klappt nun die Multiplikation mit beliebigen numerischen Typen.
```{julia}
z3, 3z3
```
```{julia}
(3.0+2im) * (12⋖30.3), 12sqrt(z2)
```
:::{.callout-caution icon="false" collapse="true" .titlenormal}
## Zusammenfassung: unser Typ `PComplex`
```julia
struct PComplex{T <: AbstractFloat} <: Number
r :: T
ϕ :: T
function PComplex{T}(r::T, ϕ::T) where T<:AbstractFloat
if r<0 # flip the sign of r and correct phi
r = -r
ϕ += π
end
if r==0 ϕ=0 end # normalize r=0 case to phi=0
ϕ = mod(ϕ, 2π) # map phi into interval [0,2pi)
new(r, ϕ) # new() ist special function,
end # available only inside inner constructors
end
# additional constructors
PComplex(r::T, ϕ::T) where {T<:AbstractFloat} = PComplex{T}(r,ϕ)
PComplex{T}(r::T1, ϕ::T2) where {T<:AbstractFloat, T1<:Real, T2<: Real} =
PComplex{T}(convert(T, r), convert(T, ϕ))
PComplex(r::T1, ϕ::T2) where {T1<:Real, T2<: Real} =
PComplex{promote_type(Float64, T1, T2)}(r, ϕ)
PComplex{T}(r::S) where {T<:AbstractFloat, S<:Real} =
PComplex{T}(convert(T, r), convert(T, 0))
PComplex(r::S) where {S<:Real} =
PComplex{promote_type(Float64, S)}(r, 0.0)
PComplex{T}(z::Complex{S}) where {T<:AbstractFloat, S<:Real} =
PComplex{T}(abs(z), angle(z))
PComplex(z::Complex{S}) where {S<:Real} =
PComplex{promote_type(Float64, S)}(abs(z), angle(z))
# nice input
⋖(r::Real, ϕ::Real) = PComplex(r, π*ϕ/180)
# nice output
using Printf
function Base.show(io::IO, z::PComplex)
# wir drucken die Phase in Grad, auf Zehntelgrad gerundet,
p = z.ϕ * 180/π
sp = @sprintf "%.1f" p
print(io, z.r, "⋖", sp, '°')
end
# arithmetic
Base.sqrt(z::PComplex) = PComplex(sqrt(z.r), z.ϕ / 2)
Base.:*(x::PComplex, y::PComplex) = PComplex(x.r * y.r, x.ϕ + y.ϕ)
# promotion rules
Base.promote_rule(::Type{PComplex{T}}, ::Type{S}) where
{T<:AbstractFloat,S<:Real} = PComplex{promote_type(T,S)}
Base.promote_rule(::Type{PComplex{T}}, ::Type{Complex{S}}) where
{T<:AbstractFloat,S<:Real} = PComplex{promote_type(T,S)}
```
:::
:::{.content-hidden unless-format="xxx"}
Jetzt geht sowas wie `PComplex(1, 0)` noch nicht. Wir wollen auch andere reelle Typen für `r` und `ϕ` zulassen. Der Einfachheit halber wandeln wir hier alles nach `Float64` um. Analog verfahren wir auch, wenn nur ein reelles oder komplexes Argument verwendet wird.
```julia
PComplex(r::Real, ϕ::Real) = PComplex(Float64(r), Float64(ϕ))
PComplex(r::Real) = PComplex(Float64(r), 0.0)
PComplex(z::Complex) = PComplex(abs(z), angle(z))
z3 = PComplex(-2); z4 = PComplex(3im)
@show z3 z4;
```
:::

613
chapters/syntax.qmd Normal file
View File

@ -0,0 +1,613 @@
# Grundlagen der Syntax
## Namen von Variablen, Funktionen, Typen etc.
- Namen können Buchstaben, Ziffern, den Unterstrich `_` und das Ausrufezeichen `!` enthalten.
- Das erste Zeichen muss ein Buchstabe oder ein Unterstrich sein.
- Groß- und Kleinbuchstaben werden unterschieden: `Nmax` und `NMAX` sind verschiedene Variablen.
- Als Zeichensatz wird [Unicode](https://home.unicode.org/) verwendet. Damit stehen über 150 Schriften und zahlreiche Symbole zur Verfügung.
- Es gibt eine kurze [Liste reservierter Schlüsselwörter](https://docs.julialang.org/en/v1/base/base/#Keywords): `if, then, function, true, false,...`
:::{.callout-tip}
## Beispiel
zulässig: `i, x, Ω, x2, DieUnbekannteZahl, neuer_Wert, 🎷, Zähler, лічильник, einself!!!!,...`
unzulässig: `Uwe's_Funktion, 3achsen, A#B, $this_is_not_Perl, true,...`
:::
----
:::{.callout-note }
## Anmerkung
Neben den *reserved keywords* der Kernsprache sind zahlreiche weitere Funktionen und Objekte vordefiniert, wie z.B. die mathematischen Funktionen `sqrt(), log(), sin()`.
Diese Definitionen finden sich in dem Modul `Base`, welches Julia beim Start automatisch lädt.
Namen aus `Base` können umdefiniert werden, solange sie noch nicht verwendet wurden:
```{julia}
#| error: true
log = 3
1 + log
```
Jetzt ist natürlich der Logarithmus kaputt:
```{julia}
#| error: true
x = log(10)
```
(siehe auch <https://stackoverflow.com/questions/65902105/how-to-reset-any-function-in-julia-to-its-original-state>)
:::
## Anweisungen
- Im Normalfall enthält eine Zeile eine Anweisung.
- Wenn eine Anweisung am Zeilenende als unvollständig erkennbar ist durch
- offene Klammern
- Operationszeichen,
dann wird die nächste Zeile als Fortsetzung aufgefasst.
- Mehrere Anweisungen pro Zeile können durch Semikolon getrennt werden.
- Im interaktiven Betrieb (REPL oder Notebook) unterdrückt ein Semikolon nach der letzten Anweisung die Ausgabe des Ergebnisses dieser Anweisung.
:::{.callout-tip}
## Beispiel
Im interaktiven Betrieb wird der Wert der letzten Anweisung auch ohne explizites `print()` ausgegeben:
```{julia}
println("Hallo 🌍!")
x = sum([i^2 for i=1:10])
```
Das Semikolon unterdrückt das:
```{julia}
println("Hallo 🌍!")
x = sum([i^2 for i=1:10]);
```
:::
---------
:::{.callout-warning }
Bei mehrzeiligen Anweisungen muss die fortzusetzende Zeile mit einer offenen Operation oder Klammer enden:
```{julia}
x = sin(π/2) +
3 * cos(0)
```
Also geht das Folgende schief, aber leider **ohne eine Fehlermeldung**!
```{julia}
#| error: true
#| warning: true
x = sin(π/2)
+ 3 * cos(0)
println(x)
```
Hier wird das `+` in der zweiten Zeile als Präfix-Operator (Vorzeichen) interpretiert. Damit sind 1. und 2. Zeile jeweils für sich vollständige, korrekte Ausdrücke (auch wenn die 2. Zeile natürlich völlig nutzlos ist) und werden auch so abgearbeitet.
Moral: Wenn man längere Ausdrücke auf mehrere Zeilen aufteilen will, sollte man immer eine Klammer aufmachen. Dann ist egal, wo der Zeilenumbruch ist:
```{julia}
x = ( sin(π/2)
+ 3 * cos(0) )
println(x)
```
:::
## Kommentare
Julia kennt 2 Arten von Kommentaren im Programmtext:
```{julia}
# Einzeilige Kommentare beginnen mit einem Doppelkreuz.
x = 2 # alles vom '#' bis zum Zeilenende ist ein Kommentar und wird ignoriert. x = 3
```
```{julia}
#=
Ein- und mehrzeilige Kommentare können zwischen #= ... =# eingeschlossen werden.
Dabei sind verschachtelte Kommentare möglich.
#=
d.h., anders als in C/C++/Java endet der Kommentar nicht mit dem ersten
Kommentar-Endezeichen, sondern die #=...=# - Paare wirken wie Klammern.
=#
Der automatische 'syntax highlighter' weiss das leider noch nicht, wie die wechselnde
Graufärbung dieses Kommentars zeigt.
=#
x #= das ist ein seltener Variablenname! =# = 3
```
## Datentypen Teil I
- Julia ist eine [stark typisierte](https://de.wikipedia.org/wiki/Starke_Typisierung) Sprache. Alle Objekte haben einen Typ. Funktionen/Operationen erwarten Argumente mit dem richtigen Typ.
- Julia ist eine [dynamisch typisierte](https://de.wikipedia.org/wiki/Dynamische_Typisierung) Sprache. Variablen haben keinen Typ. Sie sind Namen, die durch Zuweisung `x = ...` an Objekte gebunden werden können.
- Wenn man vom „Typ einer Variablen“ spricht, meint man den Typ des Objektes, das der Variablen gerade zugewiesen ist.
- Funktionen/Operatoren können verschiedene *methods* für verschiedene Argumenttypen implementieren.
- Abhängig von den konkreten Argumenttypen wird dann bei Verwendung einer Funktion entschieden, welche Methode benutzt wird ([*dynamic dispatch*](https://en.wikipedia.org/wiki/Dynamic_dispatch)).
Einfache Basistypen sind z.B.:
```
Int64, Float64, String, Char, Bool
```
```{julia}
x = 2
x, typeof(x), sizeof(x)
```
```{julia}
x = 0.2
x, typeof(x), sizeof(x)
```
```{julia}
x = "Hallo!"
x, typeof(x), sizeof(x)
```
```{julia}
x = 'Ω'
x, typeof(x), sizeof(x)
```
```{julia}
x = 3 > π
x, typeof(x), sizeof(x)
```
- `sizeof()` liefert die Größe eines Objekts oder Typs in Bytes (1 Byte = 8 Bit)
- 64bit Ganzzahlen und 64bit Gleitkommazahlen entsprechen dem Befehlssatz moderner Prozessoren und sind daher die numerischen Standardtypen.
- Zeichen/*chars* `'A'` und Zeichenketten/*strings* `"A"` der Länge 1 sind verschiedene Objekte.
## Ablaufsteuerung
### `if`-Blöcke
- Ein `if`-Block *kann* **beliebig viele** `elseif`-Zweige und als letztes maximal **einen** `else`-Zweig enthalten.
- Der Block hat einen Wert, den Wert der letzten ausgeführten Anweisung.
```{julia}
x = 33
y = 44
z = 34
if x < y && z != x # elseif- und else-Blöcke sind optional
println("yes")
x += 10
elseif x < z # beliebig viele elseif-Blöcke
println(" x is smaller than z")
elseif x == z+1
println(" x is successor of z")
else # maximal ein else-Block
println("Alles falsch")
end # Wert des gesamten Blocks ist der Wert der
# letzten ausgeführten Auswertung
```
Kurze Blöcke kann man in eine Zeile schreiben:
```{julia}
if x > 10 println("x is larger than 10") end
```
Der Wert eines `if`-Blocks kann natürlich zugewiesen werden:
```{julia}
y = 33
z = if y > 10
println("y is larger than 10")
y += 1
end
z
```
### Auswahloperator (ternary operator) `test ? exp1 : exp2`
```{julia}
x = 20
y = 15
z = x < y ? x+1 : y+1
```
ist äquivalent zu
```{julia}
z = if x < y
x+1
else
y+1
end
```
## Vergleiche, Tests, Logische Operationen
### Arithmetische Vergleiche
- `==`
- `!=`, `≠`
- `>`
- `>=`, `≥`
- `<`
- `<=`, `≤`
Wie üblich, ist der Test auf Gleichheit `==` vom Zuweisungsoperator `=` zu unterscheiden. Vergleichen lässt sich so gut wie alles:
```{julia}
"Aachen" < "Leipzig", 10 ≤ 10.01, [3,4,5] < [3,6,2]
```
Nun ja, fast alles:
```{julia}
3 < "vier"
```
Die Fehlermeldung zeigt ein paar Grundprinzipien von Julia:
- Operatoren sind auch nur Funktionen: `x < y` wird zum Funktionsaufruf `isless(x, y)`.
- Funktionen (und damit Operatoren) können verschiedene *methods* für verschiedene Argumenttypen implementieren.
- Abhängig von den konkreten Argumenttypen wird beim Aufruf der Funktion entschieden, welche Methode benutzt wird ([*dynamic dispatch*](https://en.wikipedia.org/wiki/Dynamic_dispatch)).
Man kann sich alle Methoden zu einer Funktion anzeigen lassen. Das gibt einen Einblick in das komplexe Typssystem von Julia:
```{julia}
methods(<)
```
Zuletzt noch: Vergleiche können gekettet werden.
```{julia}
10 < x ≤ 100 # das ist äquivalent zu
# 10 < x && x ≤ 100
```
### Tests
Einge Funktionen vom Typ `f(c::Char) -> Bool`
```{julia}
isnumeric('a'), isnumeric('7'), isletter('a')
```
und vom Typ `f(s1::String, s2::String) -> Bool`
```{julia}
contains("Lampenschirm", "pensch"), startswith("Lampenschirm", "Lamb"), endswith("Lampenschirm", "rm")
```
- Die Funktion `in(item, collection) -> Bool` testet, ob `item` in `collection` ist.
- Sie hat auch das Alias ` ∈(item, collection)` und
- sowohl `in` als auch `∈` können auch als Infix-Operatoren geschrieben werden.
```{julia}
x = 3
x in [1, 2, 3, 4, 5]
```
```{julia}
x ∈ [1, 2, 33, 4, 5]
```
### Logische Operationen: `&&`, `||`, `!`
```{julia}
3 < 4 && !(2 > 8) && !contains("aaa", "b")
```
#### Bedingte Auswertung (_short circuit evaluation_)
- in `a && b` wird `b` nur ausgewertet, wenn `a == true`
- in `a || b` wird `b` nur ausgewertet, wenn `a == false`
(i) Damit kann `if test statement end` auch als `test && statement` geschrieben werden.
(ii) Damit kann `if !test statement end` als `test || statement` geschrieben werden.
Als Beispiel^[aus der [Julia-Dokumentation](https://docs.julialang.org/en/v1/manual/control-flow/#Short-Circuit-Evaluation)] hier eine Implementierung der Fakultätsfunktion *(factorial)*:
```{julia}
function fact(n::Int)
n >= 0 || error("n must be non-negative")
n == 0 && return 1
n * fact(n-1)
end
fact(5)
```
Natürlich kann man alle diese Tests auch Variablen vom Typ `Bool` zuordnen und
diese Variablen können als Tests in `if`- und `while`-Blöcken verwendet werden:
```{julia}
x = 3 < 4
y = 5 ∈ [1, 2, 5, 7]
z = x && y
if z # äquivalent zu: if 3 < 4 && 5 in [1,2,5,7]
println("Stimmt alles!")
end
```
- In Julia müssen alle Tests in einem logischen Ausdruck vom Typ `Bool` sein.
- Es gibt keine implizite Konvertierung à la *"0 is false and 1 (or anything != 0) is true"*
- Wenn `x` ein numerischer Typ ist, dann muss daher das C-Idiom `if(x)` als `if x != 0` geschrieben werden.
- Es gibt eine Ausnahme zur Unterstützung der _short circuit evaluation_:
- bei den Konstrukten `a && b && c...` bzw `a || b || c...` muss der letzte Teilausdruck nicht vom Typ `Bool` sein, wenn diese Konstrukte nicht als Tests in `if` oder `while` verwendet werden:
```{julia}
z = 3 < 4 && 10 < 5 && sqrt(3^3)
z, typeof(z)
```
```{julia}
z = 3 < 4 && 10 < 50 && sqrt(3^3)
z, typeof(z)
```
## Schleifen *(loops)*
### Die `while` ("solange")-Schleife
Syntax:
```
while *condition*
*loop body*
end
```
Eine Reihe von Anweisungen (der Schleifenkörper) wird immer wieder abgearbeitet, solange eine Bedingung erfüllt ist.
```{julia}
i = 1 # typischerweise braucht der Test der
# while-Schleife eine Vorbereitung ...
while i < 10
println(i)
i += 2 # ... und ein update
end
```
Der Körper einer `while`- und `for`-Schleife kann die Anweisungen `break` und `continue` enthalten. `break` stoppt die Schleife, `continue` überspringt den Rest des Schleifenkörpers und beginnt sofort mit dem nächsten Schleifendurchlauf.
```{julia}
i = 0
while i<10
i += 1
if i == 3
continue # beginne sofort nächsten Durchlauf,
end # überspringe Rest des Schleifenkörpers
println("i = $i")
if i ≥ 5
break # breche Schleife ab
end
end
println("Fertig!")
```
Mit `break` kann man auch Endlosschleifen verlassen:
```{julia}
i = 1
while true
println(2^i)
i += 1
if i > 8 break end
end
```
### `for`-Schleifen
Syntax:
```
for *var* in *iterable container*
*loop body*
end
```
Der Schleifenkörper wird für alle Items aus einem Container durchlaufen.
Statt `in` kann immer auch $\in$ verwendet werden. Im Kopf einer `for`-Schleife kann auch `=` verwendet werden.
```{julia}
for i ∈ ["Mutter", "Vater", "Tochter"]
println(i)
end
```
Oft benötigt man einen numerischen Schleifenzähler. Dafür gibt es das *range*-Konstrukt. Die einfachsten Formen sind
`Start:Ende` und `Start:Schrittweite:Ende`.
```{julia}
endwert = 5
for i ∈ 1:endwert
println(i^2)
end
```
```{julia}
for i = 1:5.5 print(" $i") end
```
```{julia}
for i = 1:2:14 print(" $i") end
```
```{julia}
for k = 14 : -2.5 : 1 print(" $k") end
```
#### Geschachtelte Schleifen _(nested loops)_
Ein `break` beendet die innerste Schleife.
```{julia}
for i = 1:3
for j = 1:3
println( (i,j) )
if j == 2
break
end
end
end
```
Man kann *nested loops* auch in einer `for`-Anweisung zusammenfassen. Dann beendet ein `break` die Gesamtschleife.
```{julia}
for i = 1:3, j=1:3 # im Prinzip dasselbe wie oben, aber:
println( (i,j) )
if j == 2
break # break bricht hier die Gesamtschleife ab
end
end
```
:::{.callout-important .titlenormalxx}
## **Wichtig:** Die Semantik ist völlig anders, als bei C-artigen `for`-Schleifen!
**Bei jedem Schleifendurchlauf wird die Laufvariable neu mit dem nächsten Element aus dem Container initialisiert.**
```{julia}
for i = 1:5
print(i," ... ")
i += 2
println(i)
end
```
-------
Die C-Semantik von `for(i=1; i<5; i++)` entspricht der `while`-Schleife:
```
i = 1
while i<5
*loop body* # hier kann auch wirksam an i rumgepfuscht werden
i += 1
end
```
:::
## Unicode
Julia verwendet Unicode als Zeichensatz. Damit können für Variablen, Funktionen etc auch Bezeichner aus nicht-lateinischen Schriften (zB Kyrillisch, Koreanisch, Sanskrit, Runen,
Emoji,...) verwendet werden. Die Frage, wie man solche Zeichen in seinem Editor eingeben kann und ob der verwendete Bildschirm-Font sie darstellen kann, ist nicht Julias Problem.
- Einige Unicode-Zeichen, z.B. `≤, ≠, ≥, π, ∈, √`, können anstelle von `<=, !=, >=, pi, in, sqrt` verwendet werden.
- über 3000 Unicode-Zeichen können in Julia in einer LaTeX-ähnlichen Weise mit der Tab-Vervollständigung eingegeben werden.
- `\alpha<TAB>` wird zu `α`,
- `\euler<TAB>` wird zu `` (Eulersche Zahl `exp(1)`, [spezielles Schreibschrift-e, `U+0212F`](https://www.htmlsymbol.com/unicode-code/212f.html))
- `\le<TAB>` wird zu `≤`,
- `\in<TAB>` wird zu `∈`,
- `\:rainbow:<TAB>` wird zu `🌈`
[Hier geht es zur Liste.](https://docs.julialang.org/en/v1/manual/unicode-input/)
## Eigenheiten und Stolperfallen der Syntax
- Man kann den Multiplikationsoperator `*` nach einer numerischen Konstanten weglassen, wenn eine Variable, Funktion oder öffnende Klammer folgt.
```
z = 3.4x + 2(x+y) + xy
```
ist daher korrektes Julia. Beachte allerdings, dass der Term `xy` als eine Variable mit dem Namen xy interpretiert wird __und nicht__ als Produkt von x und y!
- Diese Regel hat ein paar Tücken:
Das funktioniert wie erwartet:
```{julia}
e = 7
3e
```
Hier wird die Eingabe als Gleitkommazahl interpretiert -- und `3E+2` oder `3f+2` (Float32) ebenso.
```{julia}
3e+2
```
Ein Leerzeichen schafft Eindeutigkeit:
```{julia}
3e + 2
```
Das funktioniert:
```{julia}
x = 4
3x + 3
```
...und das nicht. `0x`, `0o`, `0b` wird als Anfang einer Hexadezimal-, Oktal- bzw. Binärkonstanten interpretiert.
```{julia}
3y + 0x
```
- Es gibt noch ein paar andere Fälle, bei denen die sehr kulante Syntax zu Überraschungen führt.
```{julia}
Wichtig = 21
Wichtig! = 42 # Bezeichner können auch ein ! enthalten
(Wichtig, Wichtig!)
```
```{julia}
Wichtig!=88
```
Julia interpretiert das als Vergleich `Wichtig != 88`.
Leerzeichen helfen:
```{julia}
Wichtig! = 88
Wichtig!
```
- Operatoren der Form `.*`, `.+`,... haben in Julia eine spezielle Bedeutung (*broadcasting*, d.h., vektorisierte Operationen).
```{julia}
1.+2.
```
Wieder gilt: Leerzeichen schaffen Klarheit!
```{julia}
1. + 2.
```

720
chapters/types.qmd Normal file
View File

@ -0,0 +1,720 @@
---
notebook-links: false
---
# Das Typsystem von Julia
Man kann umfangreiche Programme in Julia schreiben, ohne auch nur eine einzige Typdeklaration verwenden zu müssen. Das ist natürlich Absicht und soll die Arbeit der Anwender vereinfachen.
Wir blicken jetzt trotzdem mal unter die Motorhaube.
## Die Typhierarchie am Beispiel der numerischen Typen
Das Typsystem hat die Struktur eines Baums, dessen Wurzel der Typ `Any` ist. Mit den Funktionen `subtypes()` und `supertype()` kann man den Baum erforschen. Sie zeigen alle Kinder bzw. die Mutter eines Knotens an.
```{julia}
subtypes(Int64)
```
Das Ergebnis ist eine leere Liste von Typen. `Int64` ist ein sogenannter **konkreter Typ** und hat keine Untertypen.
Wir klettern jetzt mal die Typhierarchie auf diesem Ast nach oben bis zur Wurzel (Informatiker-Bäume stehen bekanntlich immer auf dem Kopf).
```{julia}
supertype(Int64)
```
```{julia}
supertype(Signed)
```
```{julia}
supertype(Integer)
```
```{julia}
supertype(Real)
```
```{julia}
supertype(Number)
```
Das wäre übrigens auch schneller gegangen: Die Funktion `supertypes()` (mit Plural-s) zeigt alle Vorfahren an.
```{julia}
supertypes(Int64)
```
Nun kann man sich die Knoten angucken:
{{< embed ../notebooks/nb-types.ipynb#nb3 >}}
Mit einer kleinen rekursiven Funktion kann man schnell einen ganzen (Unter-)Baum ausdrucken:
{{< embed ../notebooks/nb-types.ipynb#nb1 >}}
::::{.content-hidden unless-format="xxx"}
...und natürlich gibt es da auch ein Julia-Paket:
{{< embed ../notebooks/nb-types.ipynb#nb2 >}}
:::
Hier das Ganze nochmal als Bild (gemacht mit LaTeX/[TikZ](https://tikz.dev/tikz-trees))
::: {.content-visible when-format="html"}
![](../images/TypeTree2.png){width=80%}
:::
::: {.content-visible when-format="pdf"}
![Die Hierarchie der numerischen Typen](../images/TypeTree2.png){width=60%}
:::
Natürlich hat Julia nicht nur numerische Typen. Die Anzahl der direkten Abkömmlinge (Kinder) von `Any` ist
```{julia}
length(subtypes(Any))
```
und mit (fast) jedem Paket, das man mit `using ...` lädt, werden es mehr.
## Abstrakte und Konkrete Typen
- Ein Objekt hat immer einen **konkreten** Typ.
- Konkrete Typen haben keine Untertypen mehr, sie sind immer „Blätter“ des Baumes.
- Konkrete Typen spezifizieren eine konkrete Datenstruktur.
:::{.xxx}
:::
- Abstrakte Typen können nicht instanziiert werden, d.h., es gibt keine Objekte mit diesem Typ.
- Sie definieren eine Menge von konkreten Typen und gemeinsame Methoden für diese Typen.
- Sie können daher in der Definition von Funktionstypen, Argumenttypen, Elementtypen von zusammengesetzten Typen u.ä. verwendet werden.
Zum **Deklarieren** *und* **Testen** der "Abstammung" innerhalb der Typhierarchie gibt es einen eigenen Operator:
```{julia}
Int64 <: Number
```
Zum Testen, ob ein Objekt einen bestimmten Typ (oder einen abstrakten Supertyp davon) hat, dient `isa(object, typ)`. Es wird meist in der Infix-Form verwendet und sollte als Frage `x is a T?` gelesen werden.
```{julia}
x = 17.2
42 isa Int64, 42 isa Real, x isa Real, x isa Float64, x isa Integer
```
Da abstrakte Typen keine Datenstrukturen definieren, ist ihre Definition recht schlicht. Entweder sie stammen direkt von `Any` ab:
```{julia}
abstract type MySuperType end
supertype(MySuperType)
```
oder von einem anderen abstrakten Typ:
```{julia}
abstract type MySpecialNumber <: Integer end
supertypes(MySpecialNumber)
```
## Die numerischen Typen `Bool` und `Irrational`
Da sie im Baum der numerischen Typen zu sehen sind, seien sie kurz erklärt:
`Bool` ist numerisch im Sinne von `true=1, false=0`:
```{julia}
true + true + true, false - true, sqrt(true), true/4
```
`Irrational` ist der Typ einiger vordefinierter Konstanten wie `π` und ``.
Laut [Dokumentation](https://docs.julialang.org/en/v1/base/numbers/#Base.AbstractIrrational) ist `Irrational` ein *"Number type representing an exact irrational value, which is automatically rounded to the correct precision in arithmetic operations with other numeric quantities".*
## Union-Typen
Falls die Baum-Hierarchie nicht ausreicht, kann man auch abstrakte Typen als Vereinigung beliebiger (abstrakter und konkreter) Typen definieren.
```{julia}
IntOrString = Union{Int64,String}
```
:::{.callout-note .titlenormal}
## Beispiel
Das Kommando `methods(<)` zeigt, dass unter den über 70 Methoden, die für den Vergleichsoperator definiert sind, einige auch *union types* verwenden, z.B. ist
```julia
<(x::Union{Float16, Float32, Float64}, y::BigFloat)
```
eine Methode für den Vergleich einer Maschinenzahl fester Länge mit einer Maschinenzahl beliebiger Länge.
:::
## Zusammengesetzte (_composite_) Typen: `struct`
Eine `struct` ist eine Zusammenstellung von mehreren benannten Feldern und definiert einen konkreten Typ.
```{julia}
abstract type Point end
mutable struct Point2D <: Point
x :: Float64
y :: Float64
end
mutable struct Point3D <: Point
x :: Float64
y :: Float64
z :: Float64
end
```
Wie wir schon bei Ausdrücken der Form `x = Int8(33)` gesehen haben, kann man Typnamen direkt als Konstruktoren einsetzen:
```{julia}
p1 = Point2D(1.4, 3.5)
```
```{julia}
p1 isa Point3D, p1 isa Point2D, p1 isa Point
```
Die Felder einer `struct` können über ihren Namen mit dem `.`-Operator adressiert werden.
```{julia}
p1.y
```
Da wir unsere `struct` als `mutable` deklariert haben, können wir das Objekt `p1` modifizieren, indem wir den Feldern neue Werte zuweisen.
```{julia}
p1.x = 3333.4
p1
```
Informationen über den Aufbau eines Typs oder eines Objekts von diesem Typ liefert `dump()`.
```{julia}
dump(Point3D)
```
```{julia}
dump(p1)
```
## Funktionen und *Multiple dispatch*
:::{.callout-note .titlenormal}
## Objekte, Funktionen, Methoden
In klassischen objektorientierten Sprachen wie C++/Java haben Objekte üblicherweise mit ihnen assoziierte Funktionen, die Methoden des Objekts.
In Julia gehören Methoden zu einer Funktion und nicht zu einem Objekt.
(Eine Ausnahme sind die Konstruktoren, also Funktionen, die genauso heißen wie ein Typ und ein Objekt dieses Typs erzeugen.)
Sobald man einen neuen Typ definiert hat, kann man sowohl neue als auch bestehende Funktionen um neue Methoden für diesen Typ ergänzen.
- Eine Funktion kann mehrfach für verschiedene Argumentlisten (Typ und Anzahl) definiert werden.
- Die Funktion hat dann mehrere Methoden.
- Beim Aufruf wird an Hand der konkreten Argumente entschieden, welche Methode genutzt wird *(multiple dispatch)*.
- Es ist typisch für Julia, dass für Standardfunktionen viele Methoden definiert sind. Diese können problemlos um weitere Methoden für eigene Typen erweitert werden.
:::
Den Abstand zwischen zwei Punkten implementieren wir als Funktion mit zwei Methoden:
```{julia}
function distance(p1::Point2D, p2::Point2D)
sqrt((p1.x-p2.x)^2 + (p1.y-p2.y)^2)
end
function distance(p1::Point3D, p2::Point3D)
sqrt((p1.x-p2.x)^2 + (p1.y-p2.y)^2 + (p1.z-p2.z)^2)
end
```
```{julia}
distance(p1, Point2D(2200, -300))
```
Wie schon erwähnt, zeigt `methods()` die Methodentabelle einer Funktion an:
```{julia}
methods(distance)
```
Das Macro `@which`, angewendet auf einen vollen Funktionsaufruf mmit konkreter Argumentliste, zeigt an, welche Methode zu diesen konkreten Argumenten ausgewählt wird:
```{julia}
@which sqrt(3.3)
```
```{julia}
z = "Hallo" * '!'
println(z)
@which "Hallo" * '!'
```
Methoden können auch abstrakte Typen als Argument haben:
```{julia}
"""
Berechnet den Winkel ϕ (in Grad) der Polarkoordinaten (2D) bzw.
Kugelkoordinaten (3D) eines Punktes
"""
function phi_winkel(p::Point)
atand(p.y, p.x)
end
phi_winkel(p1)
```
:::{.callout-tip collapse="true"}
Ein in *triple quotes* eingeschlossene Text unmittelbat vor der Funktionsdefinition
wird automatisch in die Hilfe-Datenbank von Julia integriert:
```{julia}
?phi_winkel
```
:::
Beim *multiple dispatch* wird die Methode angewendet, die unter allen passenden die spezifischste ist. Hier eine Funktion mit mehreren Methoden
(alle bis auf die letzte in der kurzen [*assignment form*](https://docs.julialang.org/en/v1/manual/functions/#man-functions) geschrieben):
```{julia}
f(x::String, y::Number) = "Args: String + Zahl"
f(x::String, y::Int64) = "Args: String + Int64"
f(x::Number, y::Int64) = "Args: Zahl + Int64"
f(x::Int64, y:: Number) = "Args: Int64 + Zahl"
f(x::Number) = "Arg: eine Zahl"
function f(x::Number, y::Number, z::String)
return "Arg: 2 x Zahl + String"
end
```
Hier passen die ersten beiden Methoden. Gewählt wird die zweite, da sie spezifischer ist, `Int64 <: Number`.
```{julia}
f("Hallo", 42)
```
Es kann sein, dass diese Vorschrift zu keinem eindeutigen Ergebnis führt, wenn man seine Methoden schlecht gewählt hat.
```{julia}
f(42, 42)
```
## Parametrisierte numerische Typen: `Rational` und `Complex`
- Für rationale Zahlen (Brüche) verwendet Julia `//` als Infix-Konstruktor:
```{julia}
@show Rational(23, 17) 4//16 + 1//3;
```
- Die imaginäre Einheit $\sqrt{-1}$ heißt `im`
```{julia}
@show Complex(0.4) 23 + 0.5im/(1-2im);
```
`Rational` und `Complex` bestehen, ähnlich wie unser `Point2D`, aus 2 Feldern: Zähler und Nenner bzw. Real- und Imaginärteil.
Der Typ dieser Felder ist allerdings nicht vollständig festgelegt. `Rational` und `Complex` sind _parametrisierte_ Typen.
```{julia}
x = 2//7
@show typeof(x);
```
```{julia}
y = BigInt(2)//7
@show typeof(y) y^48;
```
```{julia}
x = 1 + 2im
typeof(x)
```
```{julia}
y = 1.0 + 2.0im
typeof(y)
```
Die konkreten Typen `Rational{Int64}`, `Rational{BigInt}`,..., `Complex{Int64}`, `Complex{Float64}}`, ... sind Subtypen von `Rational` bzw. `Complex`.
```{julia}
Rational{BigInt} <: Rational
```
Die Definitionen [sehen etwa so aus](https://github.com/JuliaLang/julia/blob/master/base/rational.jl#L6-L15):
```{julia}
struct MyComplex{T<:Real} <: Number
re::T
im::T
end
struct MyRational{T<:Integer} <: Real
num::T
den::T
end
```
Die erste Definition besagt:
- `MyComplex` hat zwei Felder `re` und `im`, beide vom gleichen Typ `T`.
- Dieser Typ `T` muss ein Untertyp von `Real` sein.
- `MyComplex` und alle seine Varianten wie `MyComplex{Float64}` sind Untertypen von `Number`.
und die zweite besagt analog:
- `MyRational` hat zwei Felder `num` und `den`, beide vom gleichen Typ `T`.
- Dieser Typ `T` muss ein Untertyp von `Integer` sein.
- `MyRational` und seine Varianten sind Untertypen von `Real`.
Nun ist $\subset$ , oder auf julianisch `Rational <: Real`. Also können die Komponenten einer komplexen Zahl auch rational sein:
```{julia}
z = 3//4 + 5im
dump(z)
```
Diese Strukturen sind ohne das `mutable`-Attribut definiert, also *immutable*:
```{julia}
x = 2.2 + 3.3im
println("Der Realteil ist: $(x.re)")
x.re = 4.4
```
Das ist so üblich. Wir betrachten das Objekt `9` vom Typ `Int64` ja auch als unveränderlich.
Das Folgende geht natürlich trotzdem:
```{julia}
x += 2.2
```
Hier wird ein neues Objekt vom Typ `Complex{Float64}` erzeugt und `x` zur Referenz auf dieses neue Objekt gemacht.
Die Möglichkeiten des Typsystems verleiten leicht zum Spielen. Hier definieren wir eine `struct`, die wahlweise eine Maschinenzahl oder ein Paar von Ganzzahlen enthalten kann:
```{julia}
struct MyParms{T <: Union{Float64, Tuple{Int64, Int64}}}
param::T
end
p1 = MyParms(33.3)
p2 = MyParms( (2, 4) )
@show p1.param p2.param;
```
## Typen als Objekte
(1) Typen sind ebenfalls Objekte. Sie sind Objekte einer der drei "Meta-Typen"
- `Union` (Union-Typen)
- `UnionAll` (parametrisierte Typen)
- `DataType` (alle konkreten und sonstige abstrakte Typen)
```{julia}
@show 23779 isa Int64 Int64 isa DataType;
```
```{julia}
@show 2im isa Complex Complex isa UnionAll;
```
```{julia}
@show 2im isa Complex{Int64} Complex{Int64} isa DataType;
```
Diese 3 konkreten "Meta-Typen" sind übrigens Subtypen des abstrakten "Meta-Typen" `Type`.
```{julia}
subtypes(Type)
```
-----------------
(2) Damit können Typen auch einfach Variablen zugewiesen werden:
```{julia}
x3 = Float64
@show x3(4) x3 <: Real x3==Float64 ;
```
:::{.callout-note collapse="true"}
Dies zeigt auch, dass die [Style-Vorgaben in Julia](https://docs.julialang.org/en/v1/manual/style-guide/#Use-naming-conventions-consistent-with-Julia-base/) wie „Typen und Typvariablen starten mit Großbuchstaben, sonstige Variablen und Funktionen werden klein geschrieben.“ nur Konventionen sind und von der Sprache nicht erzwungen werden.
Man sollte sie trotzdem einhalten, um den Code lesbar zu halten.
:::
Wenn man solche Zuweisungen mit `const` für dauerhaft erklärt, entsteht ein
*type alias*.
```{julia}
const MyCmplxF64 = MyComplex{Float64}
z = MyComplex(1.1, 2.2)
typeof(z)
```
--------
(3) Typen können Argumente von Funktionen sein.
```{julia}
function myf(x, S, T)
if S <: T
println("$S is subtype of $T")
end
return S(x)
end
z = myf(43, UInt16, Real)
@show z typeof(z);
```
Wenn man diese Funktion mit Typsignaturen definieren möchte, kann man natürlich
```julia
function myf(x, S::Type, T::Type) ... end
```
schreiben. Üblicher ist hier die (dazu äquivalente) spezielle Syntax
```julia
function myf(x, ::Type{S}, ::Type{T}) where {S,T} ... end
```
bei der man in der `where`-Klausel auch noch Einschränkungen an die zulässigen Werte der Typvariablen `S` und `T` stellen kann.
Wie definiere ich eine spezielle Methode von `myf`, die nur aufgerufen werden soll, wenn `S` und `T` gleich `Int64` sind? Das ist folgendermaßen möglich:
```julia
function myf(x, ::Type{Int64}, ::Type{Int64}) ... end
```
`Type{Int64}` wirkt wie ein "Meta-Typ", dessen einzige Instanz der Typ `Int64` ist.
-----------------
(4) Es gibt zahlreiche Operationen mit Typen als Argumenten. Wir haben schon `<:(T1, T2)`, `supertype(T)`, `supertypes(T)`, `subtypes(T)` gesehen. Erwähnt seien noch `typejoin(T1,T2)` (nächster gemeinsamer Vorfahre im Typbaum) und Tests wie `isconcretetype(T)`, `isabstracttype(T)`, `isstructtype(T)`.
## Invarianz parametrisierter Typen {#sec-invariance}
Kann man in parametrisierten Typen auch nicht-konkrete Typen einsetzen? Gibt es `Complex{AbstractFloat}` oder `Complex{Union{Float32, Int16}}`?
Ja, die gibt es; und es sind konkrete Typen, man kann also Objekte von diesem Typ erzeugen.
```{julia}
z5 = Complex{Integer}(2, 0x33)
dump(z5)
```
Das ist eine heterogene Struktur. Jede Komponente hat einen individuellen Typ `T`, für den `T<:Integer` gilt.
Nun gilt zwar
```{julia}
Int64 <: Integer
```
aber es gilt nicht, dass
```{julia}
Complex{Int64} <: Complex{Integer}
```
Diese Typen sind beide konkret. Damit können sie in der Typhierarchie von Julia nicht in einer Sub/Supertype-Relation zueinander stehen. Julias parametrisierte Typen sind [in der Sprache der theoretischen Informatik](https://de.wikipedia.org/wiki/Kovarianz_und_Kontravarianz) **invariant**.
(Wenn aus `S<:T` folgen würde, dass auch `ParamType{S} <: ParamType{T}` gilt, würde man von **Kovarianz** sprechen.)
## Generische Funktionen
Der übliche (und in vielen Fällen empfohlene!) Programmierstil in Julia ist das Schreiben generischer Funktionen:
```{julia}
function fsinnfrei1(x, y)
return x * x * y
end
```
Diese Funktion funktioniert sofort mit allen Typen, für die die verwendeten Operationen definiert sind.
```{julia}
fsinnfrei1( Complex(2,3), 10), fsinnfrei1("Hallo", '!')
```
Man kann natürlich Typ-Annotationen benutzen, um die Verwendbarkeit einzuschränken oder um unterschiedliche Methoden für unterschiedliche Typen zu implementieren:
```{julia}
function fsinnfrei2(x::Number, y::AbstractFloat)
return x * x * y
end
function fsinnfrei2(x::String, y::String)
println("Sorry, I don't take strings!")
end
@show fsinnfrei2(18, 2.0) fsinnfrei2(18, 2);
```
:::{.callout-important}
**Explizite Typannotationen sind fast immer irrelevent für die Geschwindigkeit des Codes!**
Dies ist einer der wichtigsten *selling points* von Julia.
Sobald eine Funktion zum ersten Mal mit bestimmten Typen aufgerufen wird, wird eine auf diese Argumenttypen spezialisierte Form der Funktion generiert und compiliert. Damit sind generische Funktionen in der Regel genauso schnell, wie die spezialisierten Funktionen, die man in anderen Sprachen schreibt.
Generische Funktionen erlauben die Zusammenarbeit unterschiedlichster Pakete und eine hohe Abstraktion.
Ein einfaches Beispiel: Das Paket `Measurements.jl` definiert einen neuen Datentyp `Measurement`, einen Wert mit Fehler, und die Arithmetik dieses Typs. Damit funktionieren generische Funktionen automatisch:
```{julia}
#| echo: false
#| output: false
#=
-Measurements.jl/show.jl adds a method
Base.show(io::IO, ::MIME"text/latex", measure::Measurement)
- IJulia sends out a text/latex-cell in addition to the text/plain-cell
"outputs": [
{
"data": {
"text/latex": [
"$2590.0 \\pm 52.0$"
],
"text/plain": [
"2590.0 ± 52.0"
]
},
whenever it finds this method, see
https://github.com/JuliaLang/IJulia.jl/commit/7311e517194ddba07af64952b0f9413401213050
- Quarto takes this latex-cell in its qmd -> ipynb->exec->ipynb -> md pipeline, generating a :::{=tex} -div
- so we delete the method
=#
using Measurements
zz= @which Base.show(stdout, MIME"text/latex"(), 3±2)
Base.delete_method(zz)
```
```{julia}
using Measurements
x = 33.56±0.3
y = 2.3±0.02
fsinnfrei1(x, y)
```
:::
## Typ-Parameter in Funktionsdefinitionen: die `where`-Klausel
Wir wollen eine Funktion schreiben, die für **alle komplexen Integer** (und nur diese) funktioniert, z.B. eine [in [i] mögliche Primfaktorzerlegung](https://de.wikipedia.org/wiki/Gau%C3%9Fsche_Zahl). Die Definition
```{julia}
#| eval: false
function isprime(x::Complex{Integer}) ... end
```
liefert nun nicht das Gewünschte, wie wir in @sec-invariance gesehen haben. Die Funktion würde für ein Argument vom Typ `Complex{Int64}` nicht funktionieren, da letzteres kein Subtyp von `Complex{Integer}` ist.
Wir müssen eine Typ-Variable einführen. Dazu dient die `where`-Klausel.
```{julia}
#| eval: false
function isprime(x::Complex{T}) where {T<:Integer}
...
end
```
Das ist zu lesen als:
> „Das Argument x soll von einem der Typen `Complex{T}` sein, wobei die Typvariable `T` irgendein Untertyp von `Integer` sein kann.“
Noch ein Beispiel:
```{julia}
#| eval: false
function kgV(x::Complex{T}, y::Complex{S}) where {T<:Integer, S<:Integer}
...
end
```
> Die Argumente x und y können verschiedene Typen haben und beide müssen Subtypen von `Integer` sein.
Wenn es nur eine `where`-Klausel wie im vorletzten Beispiel gibt, kann man die geschweiften Klammern weglassen und
```{julia}
#| eval: false
function isprime(x::Complex{T}) where T<:Integer
...
end
```
schreiben. Das lässt sich noch weiter kürzen zu
```{julia}
#| eval: false
function isprime(x::Complex{<:Integer})
...
end
```
Diese verschiedenen Varianten können verwirrend sein, aber das ist nur Syntax.
```{julia}
C1 = Complex{T} where {T<:Integer}
C2 = Complex{T} where T<:Integer
C3 = Complex{<:Integer}
C1 == C2 == C3
```
Kurze Syntax für einfache Fälle, ausführliche Syntax für komplexe Varianten.
Als letztes sein bemerkt, dass `where T` die Kurzform von `where T<:Any` ist, also eine völlig unbeschränkte Typvariable einführt. Damit ist sowas möglich:
```{julia}
function fgl(x::T, y::T) where T
println("Glückwunsch! x und y sind vom gleichen Typ!")
end
```
Diese Methode erfordert, dass die Argumente genau den gleichen, aber ansonsten beliebigen Typ haben.
```{julia}
fgl(33, 44)
```
```{julia}
fgl(33, 44.0)
```

122
index.qmd Normal file
View File

@ -0,0 +1,122 @@
# Was ist Julia? {.unnumbered}
Julia ist eine noch recht junge für *scientific computing* konzipierte moderne Programmiersprache.
Ein kleines Codebeispiel:
```{julia}
using CairoMakie
a = [3, 7, 5, 3]
b = [1, 3, 7, 4]
δ = π/2
t = LinRange(-π, π, 300)
f = Figure(resolution=(800, 180))
for i in 1:4
x = sin.( a[i] .* t .+ δ )
y = sin.( b[i] .* t )
lines(f[1, i], x, y, aspect=1 )
end
f
```
## Geschichte {.unnumbered}
- 2009 Beginn der Entwicklung am *Computer Science and Artificial
Intelligence Laboratory* des MIT
- 2012 erste release v0.1
- 2018 Version v1.0
- aktuell: v1.8.5 vom 8. Januar 2023
Zum ersten release 2012 haben die Schöpfer von Julia ihre Ziele und Motivation in dem Blogbeitrag [Why we created Julia](https://julialang.org/blog/2012/02/why-we-created-julia/)
interessant zusammengefasst.
Für ein Bild von *Stefan Karpinski, Viral Shah, Jeff
Bezanson* und *Alan Edelman* bitte hier klicken: <https://news.mit.edu/2018/julia-language-co-creators-win-james-wilkinson-prize-numerical-software-1226>.
:::{.content-hidden unless-format="xxx"}
Kurzfassung:
> We want a language that is
>
> - open source
> - with the speed of C
> - obvious, familiar mathematical notation like Matlab
> - as usable for general programming as Python
> - as easy for statistics as R
> - as natural for string processing as Perl
> - as powerful for linear algebra as Matlab
> - as good at gluing programs together as the shell
> - dirt simple to learn, yet keeps the most serious hackers happy
Formale Syntax
:
- Algorithmisches Denken
- Gefühl für die Effizienz und Komplezität von Algorithmen
- Besonderheiten der Computerarithmetik, insbes. Gleitkommazahlen
- „Ökosystem“ der Sprache:
- die Kunst des Debugging.
:::
## Warum Julia? {.unnumbered}
:::{.callout-tip .titlenormal icon=false}
## aus [The fast track to Julia](https://cheatsheet.juliadocs.org/)
"Julia is an open-source, multi-platform, high-level, high-performance programming language for technical computing.
Julia has an LLVM-based JIT compiler that allows it to match the performance of languages such as C and FORTRAN without the hassle of low-level code. Because the code is compiled on the fly you can run (bits of) code in a shell or REPL , which is part of the recommended workflow .
Julia is dynamically typed, provides multiple dispatch, and is designed for parallelism and distributed computation.
Julia has a built-in package manager."
:::
*open source*
: - offene Entwicklung auf [GitHub](https://github.com/JuliaLang/julia)
- Implementierungen für alle gängigen Betriebssysteme
*high-performance programming language for technical computing*
: - viele Funktionen für *scientific computing* eingebaut,
- (bewusste) Ähnlichkeit zu Python, R und Matlab,
- komplexe Berechnungen in wenigen Zeilen
- einfaches Interface zu anderen Sprachen wie C oder Python
*a JIT compiler*
: - interaktives Arbeiten möglich: `read-eval-print loop (REPL)` mit
- just-in-time (JIT) Compilation
- dadurch Laufzeiten vergleichbar mit statischen Sprachen wie C/C++, Fortran oder Rust
*a built-in package manager*
: - riesiges *ecosystem* an einfach installierbaren Paketen, z.B.
- [Mathematische Optimierung](https://jump.dev/)
- [Machine Learning](https://fluxml.ai/)
- [Data Visualization](https://docs.makie.org/stable/)
- [Differentialgleichungen](https://docs.sciml.ai/DiffEqDocs/stable/)
- [Mathematische Modellierung](https://sciml.ai/)
## Eine kleine Auswahl an Online-Material zu Julia {.unnumbered}
- [Dokumentation](https://docs.julialang.org/en/v1/) - die offizielle Dokumentation
- [Cheat Sheet](https://juliadocs.github.io/Julia-Cheat-Sheet/) - "a quick & dirty overview"
- [Introducing Julia](https://en.wikibooks.org/wiki/Introducing_Julia)-- ein WikiBook
- [The Julia Express](http://bogumilkaminski.pl/files/julia_express.pdf) - Kurzfassung, Julia auf 16 Seiten
- [Think Julia](https://benlauwens.github.io/ThinkJulia.jl/latest/book.html) - Einführung in die Programmierung mit Julia als Sprache
- Das [Julia Forum](https://discourse.julialang.org/)
- Was fürs Auge: [Beispiele zum Julia-Grafikpaket `Makie`](https://beautiful.makie.org/dev/)